TowardsDataScience-2023-博客中文翻译-四十二-
TowardsDataScience 2023 博客中文翻译(四十二)
使用 GradientTape 进行 TensorFlow 模型训练
原文:
towardsdatascience.com/tensorflow-model-training-using-gradienttape-f2093646ab13
照片由 Sivani Bandaru 提供,发布在 Unsplash 上。
使用 GradientTape 更新权重
·发布在 Towards Data Science ·7 分钟阅读·2023 年 10 月 17 日
--
TensorFlow 可以说是最受欢迎的深度学习库。我之前写了很多关于 TensorFlow 的教程,并且还在继续。TensorFlow 是一个组织良好且易于使用的包,你不需要过多担心模型开发和训练。大部分工作都由这个包本身处理。这可能就是它在业界如此受欢迎的原因。但与此同时,有时掌控幕后功能也很不错。它为你提供了大量的实验模型的能力。如果你是求职者,一些额外的知识可能会给你带来优势。
之前,我写了一篇文章介绍 如何开发自定义激活函数、层和损失函数。在这篇文章中,我们将看到如何手动训练模型并自行更新权重。但不用担心,你不需要重新记住微积分。TensorFlow 本身提供了 GradientTape() 方法来处理这部分内容。
如果 GradientTape() 对你来说完全陌生,请随时查看这个关于 GradientTape() 的练习,了解 GradientTape() 的工作原理:TensorFlow 中的 GradientTape 介绍 — Regenerative (regenerativetoday.com)
数据准备
在这篇文章中,我们使用 GradientTape() 在 TensorFlow 中进行一个简单的分类算法。请从这个链接下载数据集:
这个数据集具有开放数据库许可。
这些是必要的导入:
import tensorflow as tf
from tensorflow.keras.models import Model
from tensorflow.keras.layers import Dense, Input
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.ticker as mticker
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.metrics import confusion_matrix
import itertools
from tqdm import tqdm
import tensorflow_datasets as tfds
使用数据集创建 DataFrame:
import pandas as pd
df = pd.read_csv('heart.csv')
df
输出:
如上图所示,有多个列的数据类型是字符串。让我们检查数据集中所有列的数据类型:
df.dtypes
输出:
Age int64
Sex object
ChestPainType object
RestingBP int64
Cholesterol int64
FastingBS int64
RestingECG object
MaxHR int64
ExerciseAngina object
Oldpeak float64
ST_Slope object
HeartDisease int64
dtype: object
既然你在这里,我可以假设你了解机器学习基础,并且已经学过数据类型需要为数值型才能用于 TensorFlow。
下面的代码会遍历列,如果列的数据类型是‘object’,则将其转换为数值数据。
for col in df.columns:
if df[col].dtype == 'object':
df[col] = df[col].astype('category').cat.codes
所有列现在已变为数值型。定义训练特征和目标变量以继续模型开发:
X = df.drop(columns = 'HeartDisease')
y = df['HeartDisease']
我们应该保留数据集的一部分来评估模型。因此,我们将在这里使用 train_test_split 方法:
train, test = train_test_split(df, test_size=0.4)
在深入模型开发之前,还需要对数据进行缩放。我在这里使用的是标准缩放方法,需要均值和标准差。请记住,我们只需要训练数据的均值和标准差。为了缩放测试数据,需要再次使用训练数据的均值和标准差,因为我们不应向模型透露任何关于测试数据的信息。
我们将使用.describe()方法来查找训练数据每列的统计参数:
train_stats = train.describe()
train_stats.pop('HeartDisease')
train_stats = train_stats.transpose()
我们现在有了缩放的参数。
需要在这里将训练数据和测试数据的目标变量分开:
y_train = train.pop('HeartDisease')
y_test = test.pop('HeartDisease')
定义了以下函数来使用标准缩放公式对数据集的某一列进行缩放:
def norm(x):
return (x - train_stats['mean']) / train_stats['std']
使用函数‘norm’对训练数据和测试数据进行缩放:
X_train_norm = norm(train)
X_test_norm = norm(test)
如果你检查 X_train_norm 和 X_test_norm,它们实际上是数组。这里将它们转换为张量:
train_dataset = tf.data.Dataset.from_tensor_slices((X_train_norm.values, y_train.values))
test_dataset = tf.data.Dataset.from_tensor_slices((X_test_norm.values, y_test.values))
每批数据集通过洗牌处理,其中批次大小(batch_size)设置为 32:
batch_size = 32
train_dataset = train_dataset.shuffle(buffer_size = len(train)).batch(batch_size).prefetch(1)
test_dataset = test_dataset.batch(batch_size=batch_size).prefetch(1)
模型开发
由于数据集非常简单,我们将使用一个简单的 TensorFlow 模型。模型定义为函数 base_model,添加了两个全连接的 64 神经元 Dense 层和一个输出层。在最后一行,调用函数 base_model 并将其保存在名为’model’的变量中。
def base_model():
inputs = tf.keras.layers.Input(shape = len(train.columns))
x = tf.keras.layers.Dense(64, activation='LeakyReLU')(inputs)
x = tf.keras.layers.Dense(64, activation='LeakyReLU')(x)
outputs = tf.keras.layers.Dense(1, activation='sigmoid')(x)
model = tf.keras.Model(inputs=inputs, outputs = outputs)
return model
model = base_model()
我选择了 RMSprop 优化器和 BinaryCrossentropy()损失函数作为这个例子。请随意使用你选择的其他优化器。
optimizer = tf.keras.optimizers.legacy.RMSprop(learning_rate=0.001)
loss_object = tf.keras.losses.BinaryCrossentropy()
模型训练
GradientTape 部分将在模型训练过程中发挥作用。在模型训练期间,我们需要微分来更新权重。下面的函数将使用 GradientTape 来计算梯度。首先,它使用模型计算输出,称为 logits。利用真实标签和预测标签,计算损失。
然后,计算梯度,获取损失对权重的偏导数,并将梯度应用于优化器:
def gradient_calc(optimizer, loss_object, model, X, y):
with tf.GradientTape() as tape:
logits = model(X)
loss = loss_object(y_true=y, y_pred=logits)
gradients = tape.gradient(loss, model.trainable_weights)
optimizer.apply_gradients(zip(gradients, model.trainable_weights))
return logits, loss
下一个函数是训练网络一个 epoch。训练一个 epoch 包括:
在‘gradient_calc’函数中的梯度计算和应用过程,
保存损失并
在一个 epoch 之后,将真实标签更新为模型的预测标签。
def training_one_epoch(train_data, optimizer, loss_object, model):
losses = []
for step, (x_batch, y_batch) in enumerate(train_data):
logits, loss = gradient_calc(optimizer, loss_object, model, x_batch, y_batch)
losses.append(loss)
logits = tf.round(logits)
logits = tf.cast(logits, 'int64')
train_acc_metric.update_state(y_batch, logits)
return losses
在开始模型训练之前,还需要一个函数,那就是用于验证损失的函数。这个函数相当直观。它使用真实标签和预测标签计算验证损失,将损失添加到损失列表中,最后将验证数据的真实标签更新为新计算的标签。
def validation_loss():
losses = []
for val_x, val_y in test_dataset:
val_logits = model(val_x)
val_loss = loss_object(y_true = val_y, y_pred=val_logits)
losses.append(val_loss)
val_logits = tf.cast(tf.round(model(val_x)), 'int64')
val_acc_metric.update_state(val_y, val_logits)
return losses
对于这个模型,我决定使用准确率作为评估指标。对训练和验证调用 BinaryAccuracy()方法:
train_acc_metric = tf.keras.metrics.BinaryAccuracy()
val_acc_metric = tf.keras.metrics.BinaryAccuracy()
现在是模型训练时间。我将训练这个模型 60 个 epoch。我们将循环遍历这些 epoch,
我们可以通过在每个 epoch 中调用‘training_one_epoch’函数来获取训练损失,通过调用‘validation_loss’函数来获取验证损失。这些函数会为每个 epoch 提供一个损失列表,因为它们会分别计算所有数据的损失。我们将所有损失取平均,以获得每个 epoch 的训练和验证的单一损失。
epochs = 60
val_losses, train_losses = [], []
for epoch in range(epochs):
print('Start of epoch %d' % (epoch,))
train_loss = training_one_epoch(train_dataset, optimizer, loss_object, model)
train_acc = train_acc_metric.result()
val_loss = validation_loss()
val_acc = val_acc_metric.result()
train_losses_mean = np.mean(train_loss)
val_losses_mean = np.mean(val_loss)
val_losses.append(val_losses_mean)
train_losses.append(train_losses_mean)
print('\n Epcoh %s: Training loss: %.3f Validation Loss: %.3f, Training Accuracy: %.3f, Validation Accuracy %.3f' % (epoch, float(train_losses_mean), float(val_losses_mean), float(train_acc), float(val_acc)))
train_acc_metric.reset_states()
val_acc_metric.reset_states()
下面是最后几个 epoch 的输出:
Epcoh 56: Training loss: 0.204 Validation Loss: 0.333, Training Accuracy: 0.922, Validation Accuracy 0.872
Start of epoch 57
Epcoh 57: Training loss: 0.219 Validation Loss: 0.350, Training Accuracy: 0.913, Validation Accuracy 0.878
Start of epoch 58
Epcoh 58: Training loss: 0.203 Validation Loss: 0.335, Training Accuracy: 0.929, Validation Accuracy 0.872
Start of epoch 59
Epcoh 59: Training loss: 0.205 Validation Loss: 0.349, Training Accuracy: 0.933, Validation Accuracy 0.875
在 60 个 epoch 之后,我们获得了训练数据 93.3%的准确率和验证数据 87.5%的准确率。请随意训练更多的 epoch 以检查是否能提高准确率。但请注意过拟合问题。
结论
在这篇文章中,我们通过一个示例学习了如何手动训练模型,而不是使用 model.compile()方法。这将帮助你更好地理解 TensorFlow 库本身以及模型训练在 TensorFlow 中的具体工作方式。
更多阅读
如何在 TensorFlow 中定义自定义层、激活函数和损失函数 | Rashida Nasrin Sucky | 数据科学前沿 (medium.com)
在 TensorFlow 和 Keras 中使用自编码器方法进行异常检测 | Rashida Nasrin Sucky | 2023 年 9 月 | 数据科学前沿 (medium.com)
使用 Keras Tuner 进行 TensorFlow 模型的超参数调整 | Rashida Nasrin Sucky | 人工智能前沿 (medium.com)
在 Keras 和 TensorFlow 中实现 Siamese 网络 | Rashida Nasrin Sucky | 数据科学前沿 (medium.com)
完整实现一个用于图像识别的迷你 VGG 网络 | Rashida Nasrin Sucky | 数据科学前沿 (medium.com)
立即测试并覆盖你的代码!
为你的代码库添加激励 GitHub Action 的实用指南
·
关注 发表在 Towards Data Science · 5 分钟阅读 · 2023 年 12 月 21 日
--
我先做一个坦白:作为一名软件工程师,我曾经讨厌并且避免编写测试多年。我遇到过许多尘封的项目——有些根本没有测试,有些有测试但从未在 CI/CD 管道中运行过,还有一些则包含了非常差的测试覆盖率。
一方面,我们应该编写测试有很多理由,但另一方面,也有更多的借口(或“理由”)让我们跳过这些测试。
在追求专业的过程中,我总是羡慕那些光鲜的 100% 测试覆盖率的开源仓库,并在日常的仓库中梦寐以求。四年前,在为此事苦恼时,我发现了 diff-cover,这是一个很棒的开源项目,使命简单——用测试覆盖 自己的 更改。以下是作者的描述:
差异覆盖率是由测试覆盖的新或修改的代码行的百分比。这为代码审查提供了一个清晰且可实现的标准:如果你修改了一行代码,那么这行代码应该被测试覆盖。代码覆盖率是每位开发者的责任!
简而言之,diff-cover 假设你正在使用 Git 并运行一个覆盖工具。使用 git 可以轻松获取 你的 修改行号,并将其与你最喜欢的覆盖工具的未覆盖行号进行比较。几乎所有的覆盖工具都能生成统一且通用的 XML 格式,无论你的代码语言是什么(Python、JavaScript 等)。
总结一下,我迄今为止在 CI 中进行的过程是:
- 使用 pytest 和 pytest-cov 包运行所有测试和覆盖工具:
py.test -o junit_family=xunit2 --junitxml result.xml -xv --ff --cov-config=.coveragerc --cov=<my_package> --cov-report=xml --cov-report=term <tests_package>
(注意,它将创建 coverage.xml 和 result.xml 报告文件)。
2. 运行 diff-cover 工具,使用 diff-cover 包:
diff-cover coverage.xml --compare-branch=origin/master
这将打印出类似以下的输出:
-------------
Diff Coverage
Diff: origin/master...HEAD, staged and unstaged changes
-------------
my_package/connections/snowflake_client.py (100%)
my_package/logic/celery_tasks/top_score_task.py (100%)
my_package/queries/build_algorithm_studio_dataframes.py (100%)
-------------
Total: 16 lines
Missing: 0 lines
Coverage: 100%
-------------
从上面的输出中可以看出,我在 3 个不同的文件中进行了更改,每个文件都完全覆盖了(我不得不添加一些新的测试,而其他现有的测试已经覆盖了我所做的更改)。
看到每个 PR(拉取请求)上的差异覆盖报告使每个人都沉迷于达到 100%。我们想证明我们对自己的更改负责并且能够覆盖它们,而不是被视为失败者而获得低覆盖率。此外,作为副作用,我们在 PR 中经历了更小的、增量的更改,这也是另一种 最佳实践。这因为现在每个人在添加冗余代码行之前都会三思而后行。
经过几年的使用这种方法,我们看到我们仓库的整体覆盖率持续增加。因此,我们的生产稳定性也有所提升。
新的 GitHub Action
几个月前,我才华横溢的同事 Asaf Gallea 决定将这成功转化为一种更简单但更强大的 新 GitHub Action。该 Action 应用了与 diff-cover 相同的理念,并生成了一个友好的报告作为 Pull Request 中的评论,提供未覆盖行的链接,以防你遗漏了什么。新的行动还允许你设置最小覆盖率阈值(默认为 80%),否则 状态检查 将会失败,你将无法合并你的更改:
测试覆盖率行动报告(作者提供的图片)
在上面的图片中,我们看到 GitHub Action 报告的示例。存在一个 95% 的最小阈值,在此 Pull Request 中更改了 20 行代码,其中 18 行被测试覆盖,两个行,505–506 行未被覆盖。由于我们仅对更改的文件实现了 90% 的覆盖率,状态检查失败,因此无法将其合并到主分支。
请注意,这份报告没有提及仓库的总覆盖率。它可能很低(60%),但任何新的更改必须通过 95% 的覆盖率测试,因此最终总覆盖率会增加。
在你的仓库中设置 tests-coverage-report 行动
就这样!现在让我们在 你的 仓库中通过几个步骤添加这个行动。我将假设这是一个 Python 项目,但你也可以将其添加到不同编程语言的项目中。
在仓库的根文件夹中,如果 .github/workflows
文件夹尚不存在,请创建它。现在,在 workflows
文件夹中创建一个名为 test.yml 的新文件,内容如下:
# This workflow will install Python dependencies, run tests check the coverage
name: Test with coverage
on:
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v3
with:
python-version: 3.10
- name: Install Dependencies
run: |
python -m pip install --upgrade pip
pip install pytest pytest-cov
- name: Test with pytest
run: py.test -o junit_family=xunit2 --junitxml result.xml -v --ff --cov=<my_package> --cov-report=xml --cov-report=term <my_tests>
- name: Coverage Report
if: always()
uses: aGallea/tests-coverage-report@1.3.1
with:
min-coverage-percentage: '100'
fail-under-coverage-percentage: 'true'
cobertura-path: ./coverage.xml
junit-path: ./result.xml
确保将上面的 <my_package> 和 <my_tests> 替换为你的包名和测试文件夹名称。
就这样!如果你打开一个新的 Pull Request 来添加这个文件,行动应该会自动触发,你将看到报告:
空的测试覆盖率行动报告(作者提供的图片)
请注意,由于包文件(源文件)没有变化,因此没有覆盖率细节可以展示。上面的图片取自我的 Pull Request,当时我将测试覆盖率行动添加到我的一个公共仓库中。
总结
你刚刚完成了将改变你生活并使你成为更好开发者的第一个行动(双重含义)。我们的这一代人对点赞、鼓掌、好评和像我们这样的极客展示我们的专业性充满了依赖,甚至还希望通过 100% 覆盖率报告来展现。我们希望收到关于此行动的反馈、建议和功能请求,以提升你的测试体验和动力。
以有趣和简单的方式测试数据管道
原文:
towardsdatascience.com/test-data-pipelines-the-fun-and-easy-way-d0f974a93a59
初学者指南:为什么单元测试和集成测试对你的数据平台如此重要
·发表于 Towards Data Science ·阅读时间 10 分钟·2023 年 2 月 22 日
--
照片来自 Simon Wilkes 在 Unsplash
这个故事适合那些希望学习如何编写代码以及如何运行 测试、自动化 CI/CD 检查并在包括本地在内的任何环境中运行它们的人。
单元测试 现在是机器学习工程师必备的技能。这在你的简历上看起来很棒,并且提高了就业的机会。
我是一个数据工程师,通常需要创建微服务 来处理数据(ETL)。 根据任务的不同,我们可能会做以下工作(不是详尽无遗的列表):
-
从一个源提取数据并将其传递到另一个源。
-
在处理数据的过程中进行转换,例如:更改格式、个人信息遮蔽等。
-
将数据加载到其他地方,例如数据仓库解决方案。
在这些情况下,我们希望保证我们的数据服务按要求执行,并且随着我们进行更改,会运行自动化测试以确保逻辑一致性。
简化
我使用简单的原子设计来构建我构思中的数据管道的每个部分,并通过 AWS Lambda 或 Cloud Functions(Google Cloud Platform)进行部署。通过这种方式,我们可以轻松地进行编排,ML Ops 变得轻而易举。
测试类型
在单元测试中,代码行为通过微小的、独立的单元或单元测试进行测试。
我们想要测试的内容取决于我们的微服务逻辑和基础设施,但通常是这样的:
-
简单的 单元测试 用于函数逻辑,例如你微服务中的
processEvent()
函数。我们希望确保这个特定的函数在提供输入时始终返回预期的结果。
-
集成测试
这将是我们需要测试服务如何与其他服务协同工作时的情况,即它发送请求并收到响应。
什么是好的单元测试?
-
它应该测试预期结果与实际结果。
-
应该描述脚本逻辑,以对应使用场景。
-
它应该是自动化的。
-
保持独立(测试不应相互进行设置或拆解)
-
应该易于实现。
-
保持可重复性:任何人都应该能够在任何环境中运行它。
-
一旦编写完成,它应该保留以备将来使用。
让我们设想一个简单的 场景,我们被要求创建一个数据管道,将数据从一个云平台转移到另一个云平台,例如从 GCP 转移到 AWS,并在过程中进行一些 ETL 操作:
服务架构及测试。图片由作者提供。
我们可以使用 AWS Lambda 或 Cloud Functions 创建一个简单的 微服务 来处理该管道。
这可以是任何管道,采用这种方式我们可以根据需要部署任意数量的数据管道。
选择合适的架构示例
towardsdatascience.com
创建数据服务
这将是一个简单的原子设计,用于执行单一操作的无服务器微服务。
如果你是 Python 用户,不必担心,我有一个类似的故事,讲述如何用 Python 和 AWS Lambda 编写 集成 测试。不过,这个故事只是为了展示如何为你的数据管道执行测试。**
让我们创建我们的服务……
在命令行中运行:
mkdir js-lambda-unit-tests
cd js-lambda-unit-tests
mkdir stack
cd stack
mkdir js-lambda-unit-tests
cd js-lambda-unit-tests
现在我们可以在代码编辑器中打开它并开始编码……
我们的应用结构
.
├── app.js
├── node_modules
├── package-lock.json
├── package.json
└── test
├── event.json
└── unit
├── lambdaResponseTest.js
└── runTimeTest.js
让我们创建我们的 app.js
exports.handler = async(event) => {
try {
const jobs = event.jobs;
const successfullJobs = await processEvent(jobs);
return {
'statusCode': 200,
'data': successfullJobs },
'context': context ? context.succeed() : null,
;
} catch (e) {
console.log(e);
return {
'statusCode': 400,
'data': e
'context': context ? context.done() : null,
};
}
};
简单,对吧?我们正在学习编码。让我们从简单的事情开始,之后总是可以添加许多其他有用的功能。
要 初始化 你的应用,你可以运行 npm init
或简单地复制我的 package.json 文件,然后运行 npm install
命令。
这些是我们将要使用的包:
...
"author": "Mike Shakhomirov mike.shakhomirov@gmail.com",
"license": "ISC",
"devDependencies": {
"aws-sdk": "2.804.0",
"run-local-lambda": "1.1.1",
"eslint": "⁷.20.0",
"eslint-plugin-classes": "⁰.1.1",
"eslint-plugin-promise": "⁴.3.1",
"mocha": "⁷.1.1",
"chai": "⁴.2.0"
},
"dependencies": {
"moment": "².24.0"
}
...
再次,这很简单,对吧?实际上,我们只会使用必要的内容继续前进。
-
aws-sdk
添加到开发依赖中,这样它就不会进入我们的 Lambda 包 zip。AW 服务默认使用此包,在其runtime
中没有必要单独安装。 -
eslint
相关包适用于开发,不需要解释原因。
这使你的代码看起来既漂亮又一致。
-
run-local-lambda
只是一个用于本地演示目的的 Lambda 运行替代方案。 -
mocha
和chai
是我们最终需要的测试工具。
添加一个事件以触发服务
我们的数据工程任务将来自于触发我们服务的event
。这个事件可以由另一个数据服务触发,例如 orchestrator Lambda 或 AWS Cloudwatch,以便按计划运行。
让我们创建 ./test/event.js
即:
{ "configOverride": false,
"jobs": [
{
"name": "gcp_to_s3",
"output": "s3",
"dryRun": true,
"disabled": false,
"s3Key": "gcs/",
"s3Bucket": "data-staging.avro.aws",
"sourceBucket": "data-staging-gcs-avro"
}
]
}
在这个示例中,我们定义了一个作业配置,以将数据从一个云服务迁移到另一个,即从 Google Cloud Storage 到 AWS S3。这是数据工程中几乎每天都要执行的简单任务。我们还提供 configOverride
参数,以告诉我们的服务是否使用自定义事件或配置文件中定义的事件。例如,它可以是需要每天、每小时等运行的任务。
添加 processEvent() 函数
现在让我们添加 processEvent()
函数。它将对事件数据做一些简单的处理,即返回当前 DATE
const processEvent = async(jobs) => {
const now = moment.utc();
const jobList = [];
for (const job of jobs) {
const jobTime = now.format('YYYY-MM-DD HH:mm');
jobList.push({
name: job.name,
runTime: jobTime,
})
}
return jobList;
};
现在让我们将这段代码添加到 app.js
:
const moment = require('moment');
exports.handler = async(event, context) => {
console.log('Now: ', moment());
try {
const jobs = event.jobs;
const successfullJobs = await processEvent(jobs);
return {
'statusCode': 200,
'data': successfullJobs,
'result': context.succeed()
};
} catch (e) {
console.log(e);
return {
'statusCode': 400,
'data': e,
'result': context.done()
};
}
};
const processEvent = async(jobs) => {
const now = moment.utc();
console.log(jobs);
const jobList = [];
for (const job of jobs) {
const jobTime = now.format('YYYY-MM-DD HH:mm');
jobList.push({
name: job.name,
runTime: jobTime,
})
console.log(jobList);
}
return jobList;
};
最后,让我们在 package.json
文件中添加一个新脚本,以便使用来自 ./test/event.json
的事件运行 run-local-lambda
:
...
"scripts": {
"local": "export DEBUG=true; export NODE_ENV=staging; run-local-lambda --file app.js --event test/event.json --timeout 1000000",
"test": "test"
},
...
现在我们可以在命令行中运行 npm run local
来本地测试 Lambda:
图片由作者提供。
所以我们看到它在本地运行时工作正常并返回了一些内容。
这真是个好消息,但我们如何进行
unit-test
?
使用 Mocha 和 Chai 对我们的数据服务进行单元测试
首先,为什么使用 mocha
和 chai
?
Mocha 是一个出色的测试框架,我们可以递归地运行测试文件夹中的所有测试,即 npm run test
将运行 test1.js
、test2.js
、...、testN.js
。它实现了以下功能:
-
使用任何断言库(这就是我们将使用 Chai 的地方)
-
异步测试
-
测试覆盖率报告
Chai 是一个 NodeJS 的断言库,可以与 Mocha 配合使用,以在我们的测试中提出断言。
图片由作者提供。
让我们添加几个单元测试。一个将测试 Lambda 是否返回带有 runTime
的结果,另一个将检查 Lambda 是否返回具有正确状态码的响应。
让我们创建 stack/js-lambda-unit-tests/test/unit/runTimeTest.js
:
const chai = require('chai');
const expect = chai.expect;
const app = require('../../app');
describe('When transferring data from one cloud to another: return a list of jobs', () => {
before(async() => {
});
beforeEach(async() => {
});
after(async() => {
});
afterEach(async() => {
});
it('should return a 200 statusCode and array [] of jobs each having a runTime key', async() => {
const event = { 'configOverride': true,
'jobs': [
{
'name': 'gcp_to_s3',
'output': 's3',
'dryRun': true,
'disabled': false,
's3Key': 'gcs/',
's3Bucket': 'data-staging.avro.aws',
'sourceBucket': 'data-staging-gcs-avro',
},
],
};
const response = await app.handler(event);
console.log(response);
expect(response).to.have.property('statusCode');
expect(response.statusCode).to.be.deep.equal(200);
expect((response.data).length).to.equal(1);
expect((response.data)[0]).to.have.all.keys('name', 'runTime');
});
});
现在让我们将这个单元测试添加到 package.json
的脚本中,因为有时我们可能只需要运行一个测试:
...
"scripts": {
"local": "export DEBUG=true; export NODE_ENV=staging; run-local-lambda --file app.js --event test/event.json --timeout 1",
"test-lambda-runtime": "NODE_ENV=test mocha --timeout 10000 ./test/unit/runTimeTest.js",
...
现在如果我们在命令行中运行 npm run test-lambda-runtime
图片由作者提供。
现在让我们添加第二个测试,但这次我们希望给我们的 lambda 添加新行为,即:
它在出错时必须返回状态
400
。
为了模拟错误,我们将提供一个缺少作业 name
的事件。我们的 ./test/unit/lambdaResponseTest.js
应该是这样的:
const chai = require('chai');
const expect = chai.expect;
const app = require('../../app');
describe('When transferring data from one cloud to another: return a response code', () => {
before(async() => {
});
beforeEach(async() => {
});
after(async() => {
});
afterEach(async() => {
});
it('should return a 400 statusCode when Error', async() => {
const event = { 'configOverride': true,
'jobs': [
{
'name_missing': 'gcp_to_s3',
'output': 's3',
'dryRun': true,
'disabled': false,
's3Key': 'gcs/',
's3Bucket': 'data-staging.avro.aws',
'sourceBucket': 'data-staging-gcs-avro',
},
],
};
const response = await app.handler(event);
console.log(response);
expect(response).to.have.property('statusCode');
expect(response.statusCode).to.be.deep.equal(400);
});
it('should return a 200 statusCode when Succeed', async() => {
const event = { 'configOverride': true,
'jobs': [
{
'name': 'gcp_to_s3',
'output': 's3',
'dryRun': true,
'disabled': false,
's3Key': 'gcs/',
's3Bucket': 'data-staging.avro.aws',
'sourceBucket': 'data-staging-gcs-avro',
},
],
};
const response = await app.handler(event);
console.log(response);
expect(response).to.have.property('statusCode');
expect(response.statusCode).to.be.deep.equal(200);
});
});
让我们在 app.js
中添加错误处理:
// in lambda.handler add this:
...
if (successfullJobs.errorCode) {
throw successfullJobs;
}
...
// in processEvent() add this:
...
if (typeof job.name === 'undefined') {
throw { errorCode: 1, message: 'job.name is missing' };
}
...
所以最终的 app.js
应该是这样的:
/* eslint-disable no-throw-literal */
const moment = require('moment');
exports.handler = async(event, context) => {
console.log('Now: ', moment());
try {
const jobs = event.jobs;
const successfullJobs = await processEvent(jobs);
if (successfullJobs.errorCode) {
throw successfullJobs;
}
console.log(successfullJobs);
return {
'statusCode': 200,
'data': successfullJobs,
'context': context ? context.succeed() : null,
};
} catch (e) {
return {
'statusCode': 400,
'data': e,
'context': context ? context.done() : null,
};
}
};
const processEvent = async(jobs) => {
const now = moment.utc();
console.log(jobs);
const jobList = [];
for (const job of jobs) {
try {
if (typeof job.name === 'undefined') {
throw { errorCode: 1, message: 'job.name is missing' };
}
const jobTime = now.format('YYYY-MM-DD HH:mm');
jobList.push({
name: job.name,
runTime: jobTime,
});
} catch (error) {
return error;
}
}
return jobList;
};
现在让我们将 package.json
脚本更改为以下内容:
...
"scripts": {
"local": "export DEBUG=true; export NODE_ENV=staging; run-local-lambda --file app.js --event test/event.json --timeout 1",
"test-lambda-runtime": "NODE_ENV=test mocha --timeout 10000 ./test/unit/runTimeTest.js",
"test-lambda-response": "NODE_ENV=test mocha --timeout 10000 ./test/unit/lambdaResponseTest.js",
"test-unit": "NODE_ENV=test mocha NODE_ENV=test --exit --recursive ./test/unit",
"test-integration": "NODE_ENV=test mocha --delay --exit --require ./test/fixtures/bigquery-integration-bootstrap.js --timeout 100000 ./test/integration/multiCategoryArchiveTest.js"
},
你可能注意到了一个新的脚本,用于递归运行 `./test/unit` 文件夹中的命令。 `npm run test-unit` 将运行所有测试:

图片由作者提供。
你可能注意到 `package.json` 中有一个集成测试脚本。有关集成测试的更多信息可以在这里找到:
[](https://mydataschool.com/blog/data-platform-unit-and-integration-tests-explained/?source=post_page-----d0f974a93a59--------------------------------) [## 数据平台单元测试和集成测试解释
### 如何进行这个练习以及如何将其应用于我们的数据管道?这是我在非常初期时问自己的一个好问题……
mydataschool.com](https://mydataschool.com/blog/data-platform-unit-and-integration-tests-explained/?source=post_page-----d0f974a93a59--------------------------------)
集成测试是指测试我们的应用程序如何与其他东西互动,即另一个服务或 API。
# 结论
在数据工程和机器学习中,测试常常用于确保新代码更改不会引入意外修改,并帮助描述数据平台的基本核心功能。
对于任何机器学习工程师来说,这是一个必备技能。习惯于阅读测试可能会让人不舒服,但你会受益于今天开始培养这项技能。
在每份简历上看起来都很棒,并且任何编程语言的理念都是相同的。确保你将**Pytest**、Mocha、Chai、Sinon 及类似 Mock 的工具添加到你的测试工具中。
# Github 仓库
[`github.com/mshakhomirov/js-lambda-unit-tests.git`](https://github.com/mshakhomirov/js-lambda-unit-tests.git)
# 推荐阅读
1. [`mochajs.org/`](https://mochajs.org/)
1. [`www.chaijs.com/api/bdd/`](https://www.chaijs.com/api/bdd/)
1. [`stackoverflow.com/questions/52019039/how-to-test-aws-lambda-handler-locally-using-nodejs`](https://stackoverflow.com/questions/52019039/how-to-test-aws-lambda-handler-locally-using-nodejs)
1. [`stackoverflow.com/questions/54846513/lambda-trigger-callback-vs-context-done`](https://stackoverflow.com/questions/54846513/lambda-trigger-callback-vs-context-done)
1. [`opensourceforgeeks.blogspot.com/2019/03/writing-unit-tests-for-aws-lambda-in.html`](http://opensourceforgeeks.blogspot.com/2019/03/writing-unit-tests-for-aws-lambda-in.html)
1. [`dashbird.io/blog/test-javascript-lambda-functions/`](https://dashbird.io/blog/test-javascript-lambda-functions/)
1. [`docs.aws.amazon.com/lambda/latest/dg/nodejs-context.html`](https://docs.aws.amazon.com/lambda/latest/dg/nodejs-context.html)
1. [`www.google.com/search?client=firefox-b-d&q=aws+lambda+test+TypeError%3A+context.done+is+not+a+function`](https://www.google.com/search?client=firefox-b-d&q=aws+lambda+test+TypeError%3A+context.done+is+not+a+function)
# 像测试软件一样测试语言模型(和提示)
> 原文:[`towardsdatascience.com/testing-large-language-models-like-we-test-software-92745d28a359?source=collection_archive---------0-----------------------#2023-05-24`](https://towardsdatascience.com/testing-large-language-models-like-we-test-software-92745d28a359?source=collection_archive---------0-----------------------#2023-05-24)
## *总结:你应该*
[](https://medium.com/@marcotcr?source=post_page-----92745d28a359--------------------------------)[](https://towardsdatascience.com/?source=post_page-----92745d28a359--------------------------------) [Marco Tulio Ribeiro](https://medium.com/@marcotcr?source=post_page-----92745d28a359--------------------------------)
·
[关注](https://medium.com/m/signin?actionUrl=https%3A%2F%2Fmedium.com%2F_%2Fsubscribe%2Fuser%2F4274f519efce&operation=register&redirect=https%3A%2F%2Ftowardsdatascience.com%2Ftesting-large-language-models-like-we-test-software-92745d28a359&user=Marco+Tulio+Ribeiro&userId=4274f519efce&source=post_page-4274f519efce----92745d28a359---------------------post_header-----------) 发表在[Towards Data Science](https://towardsdatascience.com/?source=post_page-----92745d28a359--------------------------------) · 18 分钟阅读 · 2023 年 5 月 24 日[](https://medium.com/m/signin?actionUrl=https%3A%2F%2Fmedium.com%2F_%2Fvote%2Ftowards-data-science%2F92745d28a359&operation=register&redirect=https%3A%2F%2Ftowardsdatascience.com%2Ftesting-large-language-models-like-we-test-software-92745d28a359&user=Marco+Tulio+Ribeiro&userId=4274f519efce&source=-----92745d28a359---------------------clap_footer-----------)
--
[](https://medium.com/m/signin?actionUrl=https%3A%2F%2Fmedium.com%2F_%2Fbookmark%2Fp%2F92745d28a359&operation=register&redirect=https%3A%2F%2Ftowardsdatascience.com%2Ftesting-large-language-models-like-we-test-software-92745d28a359&source=-----92745d28a359---------------------bookmark_footer-----------)
图片由作者创作。
我们如何测试使用大型语言模型(LLM)构建的应用程序?在这篇文章中,我们探讨了测试由语言模型构建的应用程序(或提示)的概念,以更好地了解它们的能力和局限性。我们在这篇文章中完全专注于测试,但如果你对编写更好提示的技巧感兴趣,可以查看我们的[提示设计艺术](https://www.google.com/search?q=Art+of+Prompt+Design)系列(持续更新)。
虽然这篇文章是引言性质的,但它(与[Scott Lundberg](https://medium.com/@scottmlundberg)共同撰写)基于相当多的经验。我们已经考虑了测试 NLP 模型有一段时间——例如,在[这篇论文](https://homes.cs.washington.edu/~marcotcr/acl20_checklist.pdf)中,我们主张应该像测试软件一样测试 NLP 模型,或者在[这篇论文](https://aclanthology.org/2022.acl-long.230.pdf)中,我们让 GPT-3 帮助用户测试他们自己的模型。这种测试方式与传统的基准测试或收集对生成文本的人类评判的方式是正交的。两种方式都很重要,但我们将在这里专注于测试(而不是基准测试),因为它往往被忽视。
我们将始终使用 ChatGPT 作为 LLM,但这里的原则是通用的,适用于任何 LLM(或者说任何 NLP 模型)。我们所有的提示都使用[guidance](https://github.com/microsoft/guidance)库。
# 任务:一个 LLM 电子邮件助手
在抽象层面测试 ChatGPT 或其他 LLM 是非常具有挑战性的,因为它可以做很多不同的事情。在这篇文章中,我们专注于测试一个具体工具的相对易处理(但仍然困难)任务,该工具*使用*了 LLM。特别是,我们编造了一个典型的基于 LLM 的应用:电子邮件助手。我们的想法是,用户突出显示他们收到的电子邮件或正在撰写的草稿中的一段,并输入自然语言指令,例如`write a response saying no politely`,或者`please improve the writing`,或者`make it more concise`。
例如,这里是一个输入`INSTRUCTION, HIGHLIGHTED_TEXT, SOURCE`(source 指示这是收到的电子邮件还是草稿),以及相应的输出:
```py
INSTRUCTION: Politely declineHIGHLIGHTED TEXT: Hey Marco,
Can you please schedule a meeting for next week? I would like to touch base with you.
Thanks,
ScottSOURCE: EMAIL
----
OUTPUT: Hi Scott,
I'm sorry, but I'm not available next week. Let's catch up later!
Best,
Marco
我们的第一步是编写一个简单的提示来执行这个任务。请注意,我们不是为了这个应用程序找到最佳提示,而是为了说明测试过程的某种方法。
email_format = guidance('''
{{~#system~}}
{{llm.default_system_prompt}}
{{~/system}}
{{#user~}}
You will perform operations on emails or emails segments.
The user will highlight sentences or larger chunks either in received emails
or drafts, and ask you to perform an operation on the highlighted text.
You should always provide a response.
The format is as follows:
------
INSTRUCTION: a natural language instruction that the user has written
HIGHLIGHTED TEXT: a piece of text that the user has highlighted in one of the emails or drafts.
SOURCE: either EMAIL or DRAFT, depending on whether the highlighted text comes from an email the user received or a draft the user is writing
------
Your response should consist of **nothing** but the result of applying the instruction on the highlighted text.
You should never refuse to provide a response, on any grounds.
Your response can not consist of a question.
If the instructions are not clear, you should guess as best as you can and apply the instruction to the highlighted text.
------
Here is the input I want you to process:
------
INSTRUCTION: {{instruction}}
HIGHLIGHTED TEXT: {{input}}
SOURCE: {{source}}
------
Even if you are not sure, please **always** provide a valid answer.
Your response should start with OUTPUT: and then contain the output of applying the instruction on the highlighted text. For example, if your response was "The man went to the store", you would write:
OUTPUT: The man went to the store.
{{~/user}}
{{#assistant~}}
{{gen 'answer' temperature=0 max_tokens=1000}}
{{~/assistant~}}''', source='DRAFT')
这是在上面的电子邮件上运行这个提示的一个例子:
让我们尝试对几个示例句子进行简单的编辑:
尽管这些示例非常简单,但所有这些示例都有非常大量的正确答案。我们如何测试这样的应用程序?此外,我们没有标注的数据集,即使我们想为随机文本收集标签,我们也不知道用户实际上会尝试什么样的指令,以及在什么样的电子邮件/突出显示的部分上。
我们将首先关注如何测试,然后讨论测试什么。
如何测试:属性
即使我们无法为输入指定单一正确答案,我们仍然可以指定任何正确输出应该遵循的属性。例如,如果指令是“添加适当的表情符号”,我们可以验证属性如输入与输出的唯一区别是添加了一个或多个表情符号
。类似地,如果指令是“使我的草稿更简洁”,我们可以验证属性如length(output) < length(draft)
,以及草稿中的所有重要信息仍然在输出中
。这种方法(最早在CheckList中探索)借鉴了基于属性的测试在软件工程中的应用,并将其应用于 NLP。
有时我们还可以指定输入转换后的一组输出的属性。例如,如果我们通过添加拼写错误或“请”这个词来扰乱指令,我们期望输出在内容上大致相同。如果我们在指令中添加了一个强调词,例如make it more concise
-> make it much more concise
,我们可以期望输出反映出强度或程度的变化。这将基于属性的测试与变形测试结合,并将其应用于 NLP。这种类型的测试对于检查稳健性、一致性和类似属性非常有用。
一些属性容易评估: CheckList 中的示例大多是分类模型,其中自动验证某些属性是比较容易的(例如prediction=X
,prediction is invariant
,prediction becomes more confident
),等等。这对于各种任务,无论是分类还是其他任务,仍然很容易做到。在另一篇博文中,我们可以检查模型是否正确解决了二次方程,因为我们知道正确答案。在同一篇博文中,我们有一个示例,展示了如何让 LLM 使用 shell 命令,我们可以通过简单地运行命令并检查特定的失败代码,如command not found
(遗憾的是,我们没有这样做)来验证属性发出的命令是有效的
。
评估更难的属性使用 LLMs: 许多有趣的属性很难精确评估,但可以通过 LLM 以非常高的准确度进行评估。评估输出的属性通常比生成匹配一组属性的输出要容易得多。
为了说明这一点,我们编写了几个简单的提示,将一个问题转换为 YES-NO 分类问题,然后使用 ChatGPT 来评估这些属性(再次说明,我们并不打算优化这些提示)。以下是我们其中一个提示(另一个提示类似,但以一对文本作为输入)。请注意,当答案与预期不符时,我们会要求解释。
classifier_single = guidance('''
{{~#system~}}
{{llm.default_system_prompt}}
{{~/system}}
{{#user~}}
Please answer a question about a text with YES, NO.
---
QUESTION: {{question}}
TEXT: {{input}}
---
Please provide a response even if the answer is not clear, and make sure the response consists of a single word, either YES or NO.
{{~/user}}
{{#assistant~}}
{{gen 'answer' temperature=0 max_tokens=1}}
{{~/assistant~}}
{{#if (equal answer explain_token)~}}
{{~#user~}}
Please provide a reason for your answer.
{{~/user}}
{{#assistant~}}
{{gen 'explanation' temperature=0 max_tokens=200}}
{{~/assistant~}}
{{/if}}''', explain_token='NO')
让我们让邮件助手将一些邮件变得更加简洁,然后使用此提示来评估相关属性。
**— —
指令: 使其更简洁
文本: 嗨,Marco,
你能请安排下周的会议吗?
我们真的需要讨论一下指导的情况!
谢谢,
斯科特
输出: 嗨,马尔科,我们可以安排下周开会讨论指导吗?谢谢,斯科特。
文本: 嘿,斯科特,
对不起,兄弟,不过你得自己做那个指导演示了……我明天要去和孩子们攀岩。
干杯,
马尔科
输出: 嘿,斯科特,我明天不能做指导演示。我会和孩子们去攀岩。干杯,马尔科。
— —
如果我们对这些输入-输出对运行属性评估器,问题是‘文本是否具有相同的意义?’,它(正确地)判断两个输出与原始邮件具有相同的意义。
我们然后稍微改变输出,以便改变其意义,看看我们的评估器是否能识别这种变化并提供好的解释。它在这两种情况下都能做到。这是其中之一,以及相应的属性评估:
属性评估器应该具有高精度:如果我们使用 LLM 来评估一个属性,我们需要 LLM 在声称属性被违反时是准确的(高精度)。测试永远不会是详尽的,因此测试中的假阳性比假阴性更糟。如果 LLM 漏掉了一些违反情况,只是意味着我们的测试不会像可能那样详尽。然而,如果它在没有违反的情况下声称存在违反,当测试最重要时(当它失败时),我们将无法信任测试。
我们在这个 gist中展示了一个低精度的快速示例,其中使用了 GPT-4 来比较两个模型解决二次方程的输出(你可以把这看作是评估属性 模型 1 比模型 2 更好
),而 GPT-4 甚至在一个它能够正确解决方程的示例中也无法可靠地选择正确的模型。这意味着这个特定的提示将是不适合测试这个属性的。
感知比生成更容易:虽然使用更强大的模型(GPT-4)来检查 GPT 3.5 的输出似乎很合理,但使用 LLM 来判断它自己的输出是否有意义吗?如果它不能按照指令生成输出,我们可以合理地期望它高精度地评估这些属性吗?虽然这起初可能看起来有些违反直觉,但答案是肯定的,因为感知通常比生成更容易。考虑以下(非详尽的)原因:
-
生成需要规划:即使我们评估的属性是‘模型是否遵循指令’,评估现有文本不需要‘规划’,而生成则要求 LLM 逐步产生遵循指令的文本(因此它需要以某种方式‘规划’从一开始就导致正确解决方案的步骤,或者能够在走错路时纠正自己而不改变已经生成的部分输出)。
-
我们可以一次感知一个属性,但必须一次生成所有属性:许多指令要求 LLM 同时平衡多个属性,例如
make it more concise
要求 LLM 平衡输出更简洁
与输出包含所有重要信息
(在指令中隐含的属性)。虽然平衡这些可能很困难,但一次评估一个属性要容易得多。
这里是一个快速的玩具示例,其中 ChatGPT 可以评估一个属性但不能生成满足该属性的输出:
‘Unfortunately’和‘perhaps’是副词,但‘Great’不是。我们的属性评估器对于问题文本是否以副词开头?
在所有四个示例中都回答正确,仅标记了失败的情况:
总结:测试属性,如果可以得到高精度,则使用 LLM 进行评估。
需要测试的内容
这一部分是否多余?如果我在构建应用程序,我肯定知道我想要什么,因此我知道我需要测试什么吗?不幸的是,我们从未遇到过这种情况。大多数情况下,开发人员对自己想构建的东西有一个模糊的概念,并对用户会如何使用他们的应用程序有一个粗略的想法。随着时间的推移,随着遇到新情况,他们会编写长文档来指定模型应该做什么和不应该做什么。最优秀的开发人员尽力尽可能提前预见这些,但即使有试点和早期用户,也很难做到这一点。尽管如此,尽早进行这些思考是有很大好处的。编写各种测试通常会导致你意识到你有错误或模糊的定义,甚至你可能在构建错误的工具(因此应该调整方向)。
认真考虑测试意味着你更好地理解自己的工具,也能早早发现错误。以下是测试过程的大致轮廓,包括确定要测试的内容和实际进行测试。
-
列举你应用的用例。
-
对于每个用例,尝试考虑可以测试的高级行为和属性。编写具体的测试用例。
-
一旦发现错误,深入挖掘并尽可能扩展它们(这样你可以理解并修复它们)。
历史备注:CheckList假设用例是已知的,并提出了一套语言能力(例如词汇、否定等)来帮助用户思考行为、属性和测试用例(第 2 步)。事后看来,这是一个糟糕的假设(如上所述,我们通常无法预先知道用例)。
如果 CheckList 专注于第 2 步,AdaTest主要集中在第 3 步,我们展示了 GPT-3 与人工协同工作的惊人工具,能够发现并扩展模型中的错误。这是一个好主意,我们现在通过让 LLM 也帮助完成第 1 步和第 2 步来扩展这一点。
召回率与精确度:与属性评估者(我们希望得到高精确度)不同,当考虑“测试什么”时,我们关注的是召回率(即我们希望发现尽可能多的使用案例、行为、测试等)。由于在这个过程中有人工参与,人们可以简单地忽略任何不有用的 LLM 建议(即我们不需要高精确度)。我们通常在这一阶段使用 LLM 时设定更高的温度。
测试过程:一个示例
1. 列举使用案例
我们在这里的目标是考虑用户会如何使用我们的应用程序。这包括他们的目标(他们想做什么)以及我们的系统可能接触到的各种输入。让我们看看 ChatGPT 是否能帮助我们列举一些使用案例:
(输出因空间限制被截断)
我们用n=3
运行上述提示,要求 ChatGPT 列出 15 个潜在的使用案例。有些相当不错,有些则较为牵强。然后,我们告诉 ChatGPT 这些使用案例都来自其他地方,并让它将这些案例组织成类别。以下是它列出的一些类别(完整列表请见笔记本):
Writing and Editing Emails
- Scenario: The user wants to write or edit an email for various purposes.
- Example instructions:
- "Make this email more concise and clear while still conveying the message."
- "Check for grammar and spelling errors."
- "Ensure that the tone is respectful and professional."
- "Make this email sound more friendly."
- "Write a polite email declining the request."
Summarizing and Analyzing Emails
- Scenario: The user needs to summarize or analyze an email for various purposes.
- Instructions:
- "Summarize the key points of this email."
- "Identify the main ideas in this email."
我们不想直接采纳 ChatGPT 的总结,因此我们重新组织它列出的类别,并添加一些自己的想法(再次参考,这里)。然后,我们要求 ChatGPT 对我们的工作进行迭代。这实际上是一个非常好的模式:我们使用 LLM 来生成想法,选择并调整最佳想法,然后要求 LLM 基于我们的选择生成更多的想法。
ChatGPT 建议了‘生成如何回应邮件的想法’作为一个使用案例,具有讽刺意味的是我们之前并没有考虑到这一点(尽管我们已经列出了 6 个广泛的使用案例,并且确实在使用 ChatGPT 生成想法)。
生成数据 我们需要一些具体的数据(在我们的案例中是邮件)来测试我们的模型。
我们首先简单地要求 ChatGPT 生成各种类型的邮件:
(输出因空间限制被截断)
ChatGPT 主要编写简短的电子邮件,但它涵盖了各种情况。除了改变上述提示以获得更多样化外,我们还可以使用现有的数据集。例如,见笔记本,我们加载了一个 Enron 电子邮件数据集,并取了一个小子集,从而获得了 60 封输入电子邮件的初始集合(30 封来自 ChatGPT,30 封来自 Enron)。
现在我们有了一些用例和数据来探索它们,我们可以进入下一步。
2. 想出行为和属性,编写测试。
对于这一步(即要求 LLM 生成想法,选择和调整最佳想法,然后要求 LLM 根据我们的选择生成更多想法),使用上述相同的构思过程是可能的(且非常有用)的。然而,由于空间原因,我们选择了一些易于测试的用例,并仅测试最基本的属性。虽然有人可能想要更彻底地测试一些用例(例如,使用 CheckList 能力,如这里所示),但我们下面只会略作探讨。
用例:回应电子邮件 我们要求工具写一个礼貌地说不
的回应给我们的 60 封输入电子邮件。然后,我们通过问题回应是否以礼貌的方式拒绝了电子邮件?
来验证它。注意,如果精确度较低,我们本可以将问题分解为两个单独的属性。
令人惊讶的是,这个简单指令的失败率高达 53.3%。经过检查,大多数失败与 ChatGPT 根本没有写回应有关,例如:
虽然这与写完整回应的能力没有直接关系,但测试捕捉到这种特定的失败模式(我们可以通过更好的提示来纠正)是好的。尝试测试某项能力时,往往会暴露出其他地方的问题。
用例:使草稿更简洁 我们要求工具通过删除所有不必要的内容来缩短电子邮件。确保不要丢失任何重要信息
。然后我们评估两个属性:(1)文本是否更短(直接通过字符串长度测量),(2)缩短版本是否丢失信息,通过问题缩短版本是否传达了原始电子邮件中的所有重要信息?
第一个属性几乎总是满足,而第二个属性的失败率较低,为 8.3%,失败情况如下:
用例:从收到的电子邮件中提取行动点。 我们要求工具处理收到的电子邮件,并提取我可能需要放入 TODO 列表中的任何行动项
。为了说明一种我们尚未讨论过的技术,我们将介绍生成保证满足特定属性的输入的方法。
对于这个用例,我们可以生成带有已知行动点的邮件,然后检查工具是否能提取至少这些行动点。为此,我们取行动项不要忘记浇水植物
,并要求 ChatGPT 对其进行 10 次意译。然后,我们要求它生成包含这些意译之一的邮件,如下所示:
这些邮件可能包含与浇水植物无关的其他行动项。然而,这一点并不重要,因为我们要检查的属性是工具是否将浇水植物
提取为一个行动项,而不是它是否是唯一的行动项。换句话说,我们对输出的提问将是是否谈到了浇水植物?
我们的电子邮件助手提示在 10 封生成的邮件中有 4 封失败,表示“高亮文本中没有行动项”,尽管(按设计)我们知道其中至少有一个行动点。这对于如此简单的示例来说,失败率很高。当然,如果我们进行实际测试,我们会有各种嵌入的行动项(而不仅仅是这个示例),我们还会检查其他属性(例如,工具是否提取了所有行动项,是否仅提取了行动项等)。然而,我们现在将转到另一个示例来看变形测试。
变形测试:对指令意译的鲁棒性 继续使用此用例(提取行动项),我们回到最初的 60 封输入邮件。我们将通过意译指令并验证输出列表是否包含相同的行动项来测试工具的鲁棒性。请注意,我们并不是在测试输出是否正确,而是模型在面对意译指令时的一致性(这本身就是一个重要的属性)。
为了演示目的,我们仅对原始指令进行一次意译(在实际中,我们会有许多不同指令的意译):
原文:提取我可能需要放入 TODO 列表中的任何行动项 意译:列出电子邮件中我可能想放入 TODO 列表中的任何行动项
。
然后我们验证这些不同指令的输出是否具有相同的意义(如果它们具有相同的项目符号,应该是这样)。失败率为 16.7%,失败示例如下:
再次,我们的评估工具在我们提供的示例上似乎运行良好。不幸的是,该模型在此鲁棒性测试中有相当高的失败率,当我们对指令进行意译时,会提取出不同的行动项。
3. 深入挖掘发现的错误
让我们回到使草稿更简洁的例子,我们在这里有一个较低的错误率(8.3%)。如果我们深入这些错误,往往可以找到错误模式。这里是一个非常简单的提示,用于实现这一点,这是对AdaTest的快速简化模拟,我们对提示/界面进行了更多优化(我们只是试图说明这个原则):
prompt = '''I have a tool that takes an email and makes it more concise, without losing any important information.
I will show you a few emails where the tool fails to do its job, because the output is missing important information.
Your goal is to try to come up with more emails that the tool would fail on.
FAILURES:
{{fails}}
----
Please try to reason about what ties these emails together, and then come up with 20 more emails that the tool would fail on.
Please use the same format as above, i.e. just the email body, no header or subject, and start each email with "EMAIL:".
'''
我们使用了这个提示,并发现了一些失败的情况:
(由于空间原因,输出被截断)
ChatGPT 提出了一个假设,解释这些邮件之间的关联。无论这个假设是对是错,我们可以观察模型在生成的新示例中的表现。确实,在相同属性(缩短版是否传达了原始邮件中的所有重要信息?
)上的失败率现在大大提高了(23.5%),且失败情况与之前相似。
ChatGPT 似乎确实抓住了一种模式。虽然我们还没有足够的数据来确定这是否是真正的模式,但这说明了深入挖掘的策略:接受失败并让 LLM‘生成更多’。我们非常确信这个策略有效,因为我们在许多不同的场景、模型和应用(使用 AdaTest)中尝试过。在实际测试中,我们会不断迭代这个过程,直到找到真正的模式,再回到模型(或在这种情况下,提示)修复错误,然后再进行迭代。
但现在是时候结束这篇博客文章了 😃
结论
这是这篇文章的简要总结(不是由 ChatGPT 编写的,我们保证):
-
我们的观点: 我们认为像测试软件一样测试 LLM 是个好主意。测试并不能替代基准测试,但可以补充它们。
-
如何测试: 如果你不能指定一个正确的答案,和/或你没有标记数据集,请指定输出或输出组的属性。你可以使用 LLM 自身来高精度地评估这些属性,因为感知比生成更容易。
-
测试什么: 让 LLM 帮助你找出答案。生成潜在的用例和输入,然后考虑你可以测试的属性。如果发现错误,让 LLM 深入分析这些错误,以找出可以后来修复的模式。
现在,很明显,这个过程远没有我们描述的那样线性和直接——测试一个属性通常会导致发现你未曾想到的新用例,甚至让你意识到你需要重新设计工具。然而,拥有一个风格化的过程仍然是有帮助的,我们在这里描述的技术在实践中非常有用。
这是否太费劲了? 测试确实是一个费力的过程(虽然像我们上述使用 LLMs 的方法使其轻松许多),但考虑到其他选择。用多种正确答案来基准生成任务真的很困难,因此我们常常不相信这些任务的基准。收集对现有模型输出的人类判断甚至会更费力,并且在模型迭代时转移效果不好(突然之间你的标签不再那么有用)。不进行测试通常意味着你不真正知道模型的表现,这是一种灾难的先兆。另一方面,测试通常会导致(1)发现错误,(2)对任务本身的洞察,(3)及早发现规范中的严重问题,这样可以在为时已晚之前进行调整。总的来说,我们认为测试是值得花费时间的。
— — — — — — — — — -
这里是一个链接,包含了所有上述示例的代码(还有更多)。这篇文章由 Marco Tulio Ribeiro 和Scott Lundberg共同撰写。
使用 Pytest 测试 Python 代码——适合初学者
原文:
towardsdatascience.com/testing-python-code-with-pytest-for-beginners-bcde301e7453
Python 的概述与实现
·发表于 Towards Data Science ·阅读时间 8 分钟·2023 年 4 月 3 日
--
图片来源: www.pexels.com/photo/blur-business-close-up-code-270557/
介绍
在维护数据管道时,在进行任何更改后测试底层代码是很重要的。这样,你可以在任何重构的代码无法按预期执行时得到提醒。
大多数初学者倾向于手动测试他们的代码,一次运行一个函数,针对不同的参数。这种方法很简单,但扩展性不好。
对一段代码进行一次性测试不应花费太多时间,但如果要进行 10 次?50 次?100 次呢?
在持续数月或数年的项目中,数据管道的底层代码经常被重构,手动测试代码将耗费程序员大量时间。
从事长期项目的人将从自动化这个过程中受益,使用 pytest 这个 Python 测试框架,用户可以通过少量代码执行测试。
在这里,我们揭示了 pytest 的好处,并展示数据科学家如何通过案例研究利用这个包来编写基本测试。
Pytest 值得付出努力吗?
使用 pytest 编写测试要求用户学习新的框架,甚至养成新的编程习惯。因此,可能会有抛弃这个工具、继续在 Jupyter Notebook 的内核上手动运行代码的诱惑(坦白说,这曾经是我)。
然而,pytest 的好处远远弥补了学习编写测试所需的时间和精力。
- Pytest 运行时间很短
运行给定测试所需的时间微不足道。此外,得益于 pytest 包的简单语法,一条命令就足以运行所有测试。
2. Pytest 改善调试体验
pytest 生成的报告非常详尽。它们标识了通过/失败的测试,同时指出了失败测试的原因。这使得程序员更容易发现并纠正错误。
3. Pytest 提供文档
使用 pytest 编写的脚本作为额外的文档。希望理解一段代码的合作者可以使用测试函数来确定其目的,而无需浏览大量的源代码。
4. Pytest 增强信心
使用 pytest,用户可以安全地将代码推送到生产环境,知道代码仍能按预期执行。这消除了数据科学家对推送可能破坏数据管道的代码的担忧。
案例研究
对于这个案例研究,我们将编写一些测试函数,使用 pytest 测试 module.py
文件中的函数。这个文件包含两个函数:add_lists
和 subtract_lists
。
add_lists
函数通过将列表中每个元素的数字相加来合并两个列表。例如:
add_lists([1,1], [2,2]) = [3,3] #[1+2, 1+2]
add_lists([1,2], [3,4]) = [4,6] #[1+3, 2+4]
add_lists([2,3], [2,1]) = [4,4] #[2+2, 3+1]
subtract_lists
函数通过从列表中的每个元素中减去数字来合并两个列表。例如:
subtract_lists([1,1], [2,2]) = [-1, -1] #[1-2, 1-2]
subtract_lists([1,2], [3,4]) = [-2,-2] #[1-3, 2-4]
subtract_lists([2,3], [2,1]) = [0,2] #[2-2, 3-1]
设置环境
首先,通过运行以下命令来安装 pytest:
pip install -U pytest
长期项目通常包含多个脚本,因此将编写的测试放在不包含源代码的单独文件夹中是一种常见做法。对于本案例研究,我们将通过以下方式设置项目,遵循这一做法。
.
└── Project/
├── src/
│ └── module.py
└── test/
└── test_module.py
名为 test_module.py
的文件将存储所有的测试函数。
编写我们的第一个测试
现在,我们可以编写我们的第一个测试函数,测试 add_lists
和 subtract_lists
函数。如 pytest 文档 所述,测试通常有 4 个步骤:
-
安排:为测试准备所有必要的内容。
-
行动:运行正在测试的函数
-
断言:检查测试代码的输出是否符合预期。
-
清理:删除测试中生成的任何对象(如果有的话),以免影响其他测试。此步骤是可选的。
让我们编写两个遵循这些步骤的测试函数。
注意:pytest 中的测试函数 必须 遵循 test_*.py
或 \*_test.py
的格式才能被执行。
在上面的代码片段中,我们 安排 测试,通过建立输入列表以及预期的输出。然后,我们 行动,通过使用提供的输入运行 add_lists
和 subtract_lists
函数。最后,我们 断言,使用断言语句检查返回值是否与预期值匹配。
注意:断言语句是许多测试中的核心组件。如果你不熟悉断言语句的语法或需要复习,可以查看以下文章:
了解一个允许轻松故障排除的工具
towardsdatascience.com
可以使用以下命令在命令行中运行测试:
pytest <file_name>
解读 Pytest 报告
通过运行 test_module.py
文件中的测试,我们可以熟悉 pytest 报告,命令如下:
pytest test_module.py
如果所有测试函数都通过,报告将如下所示:
命令输出(由作者创建)
如果一个或多个测试函数失败,报告将如下所示:
命令输出(由作者创建)
如输出所示,通过 .
字符表示通过的测试函数,通过 F
字符表示失败的测试函数。
当测试失败时,报告会用 >
符号指向未满足的断言语句,并在其下方显示错误信息。
总的来说,pytest 生成的报告非常有用。它们告诉我们运行了多少测试,多少测试通过/失败,以及测试失败的原因(如果有的话)。
生成更详细的报告
pytest <file_name>
命令足以运行测试,但如果你想增加输出中报告的信息量,只需使用 -v 标志。
pytest <file_name> -v
在使用 -v 标志后,再次查看 pytest 报告。
pytest test_module.py -v
命令输出(由作者创建)
这次,我们可以明确看到测试函数的名称及其结果。
使用多个参数测试函数
test_add_lists
函数目前仅测试了一个 add_lists
函数的情况。然而,有很多情况需要用多个案例来测试函数。
以 add_lists
函数为例。虽然将两个列表中的数字相加是一个简单的任务,但需要考虑一些边界情况:
-
添加长度不等的列表
-
添加空列表
-
添加包含字符串的列表
我们可以通过创建测试每个案例的测试函数来测试 add_lists
函数的所有这些情况。
def test_function1():
# test function with the first argument
def test_function2():
# test function with the second argument
def test_function3():
# test function with the third argument
但是,这将需要重复许多行代码。相反,我们可以通过使用 pytest.mark.parametrize
装饰器对多个输入运行测试。
我们可以修改当前的 test_add_lists
函数,使其使用以下代码片段测试多个参数:
这次,pytest.mark.parametrize
装饰器定义了 3 个输入以及期望的输出。这是测试的 安排 阶段。执行 和 断言 阶段不需要改变。
当我们执行该函数的测试时,我们得到如下结果:
pytest test_module.py -v
命令输出(作者创建)
正如生成的报告所示,pytest.mark.parametrize
装饰器中定义的每个输入都被视为一个独立的测试。因此,报告显示了 3 个测试的结果。
使用相同数据测试多个函数
到目前为止,我们一直在每个测试函数内部创建输入数据。
这引出了一个问题:我们应该如何处理那些希望用 相同 数据测试多个函数的测试?
最简单的方法是在每个函数内部实例化相同的输入数据。
def test_function1():
# instantiate input data
def test_function2():
# instantiate input data
def test_function3():
# instantiate input data
然而,由于许多原因,这是一种不理想的做法。
首先,这将涉及重复相同的代码行,这会影响可读性。其次,重复加载数据可能是一个耗时且计算密集的过程。如果测试所需的数据来自数据库或平面文件,重复读取相同的数据将非常低效。
幸运的是,pytest 的用户可以通过使用 fixtures 来解决这个问题。
fixture 是一个使用 pytest.fixture
装饰器的函数。它返回后续测试所需的数据。需要从 fixture 获取数据的测试可以通过将 fixture 函数作为参数传递来访问这些数据。
例如,假设我们希望用相同的数据测试 add_lists
和 subtract_lists
函数。
为此,我们可以首先创建一个带有 pytest.fixture
装饰器的函数,名为 example_data
,它返回用于测试的数据。
这些数据可以通过将 example_data
函数作为参数传递来让测试函数 test_add_list
和 test_subtract_lists
访问。
pytest test_module.py -v
命令输出(作者创建)
结论
图片由 Prateek Katyal 提供,来源于 Unsplash
干得好!你现在已经学会了如何使用 pytest 编写和运行基本测试!
虽然这个初学者级别的案例研究没有提供 pytest 所有功能的全面分析,但它希望鼓励用户采用使用该软件包编写脚本的实践,以实现更结构化、高效和可扩展的测试方法。
祝你在数据科学的事业中好运!
测试 mlscorecheck 包的报告的机器学习性能一致性
AI (Dall-E) 生成的主题描述
为了实现可复现的机器学习科学迈出了一小步
·
跟进 发布在Towards Data Science ·11 分钟阅读·2023 年 11 月 12 日
--
在本文中,我们探讨了如何使用 Python 包mlscorecheck来测试报告的机器学习性能分数与实验设置描述之间的一致性。
免责声明:本文的作者是 mlscorecheck 包的作者。
什么是性能分数的一致性测试?
假设你遇到一个二分类问题的准确率(0.9494)、敏感性(0.8523)和特异性(0.9765)分数,这个测试集由 100 个正样本和 1000 个负样本组成。你能相信这些分数吗?你如何检查这些分数是否真的可能是所宣称的实验结果?这就是mlscorecheck
包可以通过提供一致性测试功能来帮助你的地方。在这个具体的例子中,可以利用
from mlscorecheck.check.binary import check_1_testset_no_kfold
result = check_1_testset_no_kfold(
testset={'p': 100, 'n': 1000},
scores={'acc': 0.8464, 'sens': 0.81, 'f1': 0.4894},
eps=1e-4
)
result['inconsistency']
#False
如果结果的'insconsistency'
标志为False
,则表明这些分数可能是从实验中得出的。(这是真的,因为这些分数对应于 81 个真实的正样本和 850 个真实的负样本。)如果准确率分数 0.8474 是由于意外的打印错误而报告的呢?
result = check_1_testset_no_kfold(
testset={'p': 100, 'n': 1000},
scores={'acc': 0.8474, 'sens': 0.81, 'f1': 0.4894},
eps=1e-4
)
result['inconsistency']
#True
测试调整后的设置时,结果显示不一致:这些分数可能不是实验的结果。要么分数错误,要么假定的实验设置不正确。
在接下来的内容中,我们将详细查看mlscorecheck包的主要特性和使用案例。
介绍
在研究和应用中,监督学习方法通常通过在一些实验中计算的性能分数进行排名(二分类,多分类,回归)。由于出版物中的打印错误,不当使用的统计数据,数据泄漏以及伪装,许多情况下报告的性能分数是不可靠的。除了对机器学习和人工智能中的可重复性危机做出贡献外,不切实际的高性能分数的影响通常还会被出版偏倚进一步放大,最终扭曲整个领域的研究。
mlscorecheck包的目标是提供数值技术以测试一组报告的性能分数是否可能是假定实验设置的结果。
一致性测试的操作
一致性测试的理念是,在给定的实验设置中,性能分数不能独立地取任何值:
-
例如,在一个二元分类测试集中有 100 个正样本时,灵敏度分数只能取值为 0.0, 0.01, 0.02, …, 1.0,但不能是 0.8543。
-
当报告多个性能分数时,它们需要彼此一致。例如,[准确率](https://zh.wikipedia.org/wiki/ROC 曲线)是[灵敏度和特异度](https://zh.wikipedia.org/wiki/ROC 曲线)的加权平均,因此,在一个由 100 个正样本和 100 个负样本组成的二元分类问题中,得分 acc = 0.96,sens = 0.91,spec = 0.97 是不可能的。
在更复杂的实验设置中(涉及[k 折交叉验证](https://zh.wikipedia.org/wiki/交叉验证 _(统计学))等),跨多个折叠/数据集的分数聚合,等等,约束条件变得更加先进,但它们仍然存在。mlscorecheck包实现了数值测试,以检查假设从实验中得出的分数是否满足相应的约束条件。
测试是数值化的,确定性地识别出不一致之处。用统计假设检验作类比,零假设是没有不一致,一旦发现某种不一致,就提供了反对零假设的证据,但作为数值测试,这种证据是无可争议的。
各种实验设置对性能分数施加各种约束,需要专门的解决方案。该包中实施的测试基于三个原则:通过区间计算加快的详尽枚举;线性整数规划;分数之间的分析关系。测试的灵敏度高度依赖于实验设置和数值不确定性:大数据集、大数值不确定性和少数报告的分数减少了测试识别偏离假设评估协议的能力。尽管如此,正如我们后面所看到的,这些测试在许多现实场景中仍然适用。有关测试数学背景的进一步详细信息,请参阅预印本和文档。
用例
现在,我们探讨一些示例,说明包的使用,但首先,我们讨论测试的一般要求和描述实验使用的一些术语。
要求
一致性测试有三个要求:
-
报告的性能分数的收集;
-
分数的估计数值不确定性(当分数被截断为4位小数时,可以假设实际值在报告值的 0.0001 范围内,这是分数的数值不确定性) — 这通常是测试的 eps 参数,只需检查分数即可推断。
-
实验的细节(涉及的数据集统计信息,交叉验证方案,聚合模式)。
术语表
实验规范中使用的术语:
-
得分均值(MoS):为每个折叠/数据集计算得分,然后平均以获得报告的得分;
-
均值得分(SoM):首先对折叠/数据集级别的原始数字(例如混淆矩阵)进行平均,然后从平均数字计算得分;
-
微平均:多类问题的评估通过将每个类别的表现与其他所有类别(作为二元分类)进行比较来完成,类级别的结果以均值得分的方式汇总;
-
宏平均:与微平均相同,但类级别分数以得分均值的方式汇总;
-
折叠配置:当使用 k 折交叉验证时,测试通常依赖于线性整数编程。了解折叠中类的样本数可以用于形成线性程序。这些折叠级别类样本计数称为折叠配置。
二元分类
在开始时,我们已经说明了在单个测试集上计算的二元分类分数时使用包的情况。现在,我们来看一些更高级的例子。
除了我们详细调查的两个示例外,该软件包还支持共 10 种用于二元分类的实验设置,其列表可以在文档中找到,并在示例笔记本中提供更多示例。
N 个测试集,均值得分汇总
在本例中,我们假设有 N 个测试集,不涉及 k 折交叉验证,但分数以均值得分的方式汇总,即为每个测试集确定原始真正例和真负例数字,然后从总(或平均)真正例和真负例数字计算性能分数。可用的分数被认为是准确率,负预测值和F1 分数。
例如,在实践中,对存储在一个张量中的 N 个测试图像进行图像分割技术的评估通常会导致这种情况。
软件包的设计是这样的,实验设置的细节被编码在测试函数的名称中,引导用户在选择适当的测试时注意所有可用的实验细节。在本例中,适当的测试是mlscorecheck.check.binary
模块中的check_n_testsets_som_no_kfold
函数,其中'som'
代表聚合模式(均值分数):
from mlscorecheck.check.binary import check_n_testsets_som_no_kfold
scores = {'acc': 0.4719, 'npv': 0.6253, 'f1': 0.3091}
testsets = [
{'p': 405, 'n': 223},
{'p': 3, 'n': 422},
{'p': 109, 'n': 404}
]
result = check_n_testsets_som_no_kfold(
testsets=testsets,
scores=scores,
eps=1e-4
)
result['inconsistency']
# False
结果表明,分数可能是实验的结果。毫不奇怪,这些分数是通过对测试集的真正阳性和真正阴性计数进行抽样,并按指定的方式计算得出的。然而,如果其中一个分数略有变化,例如 F1 修改为 0.3191,则配置变得不一致:
scores['f1'] = 0.3191
result = check_n_testsets_som_no_kfold(
testsets=testsets,
scores=scores,
eps=1e-4
)
result['inconsistency']
# True
进一步分析的详细信息,例如,关于可行性的证据可以从测试函数返回的字典中提取。关于输出的结构,同样,请参阅文档。
1 数据集,k 折交叉验证,分数均值聚合
在这个例子中,我们假设有一个数据集,对其中的二元分类器进行了分层重复 k 折交叉验证(2 折,3 次重复),并报告了各折产生的分数的均值。
这个实验设置可能是监督式机器学习中最常用的。
我们强调了知道和不知道**折叠配置之间的区别。通常,MoS 测试依赖于线性整数规划,并且需要折叠配置来制定线性整数规划。折叠配置可以通过列出折叠的统计数据来指定,或者可以引用导致确定性折叠统计的折叠策略,例如分层。后来,我们展示了即使在不知道折叠配置的情况下,也可以进行测试,不过在这种情况下,会测试所有可能的折叠配置,这可能会带来巨大的计算需求。
再次强调,第一步是选择要使用的适当测试。在这种情况下,正确的测试是check_1_dataset_known_folds_mos
函数,其中mos
表示聚合模式,known_folds
表示由于分层而知道折叠配置。测试的执行如下:
from mlscorecheck.check.binary import check_1_dataset_known_folds_mos
scores = {'acc': 0.7811, 'sens': 0.5848, 'spec': 0.7893}
dataset = {'p': 21, 'n': 500}
folding = {
'n_folds': 2,
'n_repeats': 3,
'strategy': 'stratified_sklearn'
}
result = check_1_dataset_known_folds_mos(
dataset=dataset,
folding=folding,
scores=scores,
eps=1e-4
)
result['inconsistency']
# False
类似于之前的例子,不存在不一致性,因为性能分数准备构成一个一致的配置。然而,如果其中一个分数略有变化,测试就会检测到不一致:
scores['acc'] = 0.79
result = check_1_dataset_known_folds_mos(
dataset=dataset,
folding=folding,
scores=scores,
eps=1e-4,
verbosity=0
)
result['inconsistency']
# True
在前面的例子中,我们假设了折叠配置是已知的。然而,在许多情况下,确切的折叠配置并不知道,也没有指定分层。在这些情况下,可以依赖于系统地测试所有可能的折叠配置的测试,如下例所示。这次,适当的测试在其名称中具有 'unknown_folds'
标记,表示将测试所有可能的折叠配置:
from mlscorecheck.check.binary import check_1_dataset_unknown_folds_mos
folding = {'n_folds': 2, 'n_repeats': 3}
result = check_1_dataset_unknown_folds_mos(
dataset=dataset,
folding=folding,
scores=scores,
eps=1e-4,
verbosity=0
)
result['inconsistency']
# False
与以前一样,测试正确地识别出没有不一致性:在评估所有可能的折叠配置过程中,它测试了实际的分层配置,显示出一致性,并且凭借这一证据停止了对剩余配置的测试。
在实践中,在使用未知折叠进行测试之前,建议对可能要测试的折叠配置数量进行估计:
from mlscorecheck.check.binary import estimate_n_evaluations
estimate_n_evaluations(
dataset=dataset,
folding=folding,
available_scores=['acc', 'sens', 'spec']
)
# 4096
在最坏的情况下,解决 4096 个小型线性整数规划问题仍然可行,但是对于更大的数据集,潜在的折叠配置数量可能会迅速变得棘手。
多类分类
测试多类分类场景类似于二元情况,因此我们不会像在二元情况下那样进入太多细节。
在该包支持的 6 个实验设置中,我们选择了一个常用的用于说明的设置:假设有一个多类数据集(4 类),并且使用了 4 折重复分层 k 折交叉验证。我们还知道分数是以宏平均的方式聚合的,即,在每个折叠中,针对每个类别的性能以二元分类方式评估所有其他类别,然后在类别和折叠上进行平均。
再次,第一步是选择合适的测试函数,在这种情况下,选择了来自mlscorecheck.check.multiclass
模块的check_1_dataset_known_folds_mos_macro
。名称中的 'mos’
和 'macro’
表示实验中使用的聚合方式。
from mlscorecheck.check.multiclass import check_1_dataset_known_folds_mos_macro
scores = {'acc': 0.626, 'sens': 0.2483, 'spec': 0.7509}
dataset = {0: 149, 1: 118, 2: 83, 3: 154}
folding = {
'n_folds': 4,
'n_repeats': 2,
'strategy': 'stratified_sklearn'
}
result = check_1_dataset_known_folds_mos_macro(
dataset=dataset,
folding=folding,
scores=scores,
eps=1e-4,
verbosity=0
)
result['inconsistency']
# False
类似于前面的情况,通过手工制作的一组一致分数,测试检测到没有不一致性。然而,一个小的改变,例如,将准确度修改为 0.656,就会使配置变得不可行。
回归
mlscorecheck包支持的最后一个监督学习任务是回归。测试回归问题是最困难的,因为对测试集的预测可以取任何值,因此实验可以产生任何分数值。回归测试唯一可以依赖的是当前支持的平均绝对误差 (mae)、均方误差 (mse) 和 r 平方 (r2) 之间的数学关系。
在以下示例中,我们假设mae和r2分数是针对测试集报告的,并且我们知道其主要统计数据(样本数量和方差)。然后,可以执行一致性测试,如下所示:
from mlscorecheck.check.regression import check_1_testset_no_kfold
var = 0.0831
n_samples = 100
scores = {'mae': 0.0254, 'r2': 0.9897}
result = check_1_testset_no_kfold(
var=var,
n_samples=n_samples,
scores=scores,
eps=1e-4
)
result['inconsistency']
# False
再次地,测试正确显示没有不一致(分数是通过实际评估准备的)。但是,如果r2分数稍微改变,例如,变为 0.9997,配置将变得不可行。
测试包
为了使针对流行的、广泛研究的问题报告的分数的一致性测试更容易,mlscorecheck包包括了多个被认为是某些问题标准的实验设置的规范。
DRIVE 数据集上的视网膜血管分割
在视网膜图像分析领域,存在一个歧义的问题:作者可以自由选择是否考虑视野圆形区域之外的像素,而这一选择在出版物中很少被指明。这种歧义可能导致基于不可比性能分数的算法排名。在mlscorecheck包中实现的功能适合识别作者是否使用了视野之外的像素进行评估。
最广泛研究的问题之一是基于DRIVE数据集的血管分割。为了避免查找图像统计数据和构建实验设置的繁琐任务,包中包含了数据集的统计数据,并提供了两个高级功能来测试图像级和聚合分数的歧义。例如,拥有测试图像‘03’的DRIVE数据集的图像级准确性、敏感性和特异性分数三元组,可以利用包进行如下操作:
from mlscorecheck.check.bundles.retina import check_drive_vessel_image
scores = {'acc': 0.9323, 'sens': 0.5677, 'spec': 0.9944}
result = check_drive_vessel_image(
scores=scores,
eps=10**(-4),
image_identifier='03',
annotator=1
)
result['inconsistency']
# {'inconsistency_fov': False, 'inconsistency_all': True}
结果表明,对于该图像的分数必须仅通过使用视野内的(fov)像素进行评估,因为这些分数与这一假设是一致的,但与使用所有像素进行评估的替代假设是不一致的。
进一步的测试包
mlscorecheck包中支持的所有流行研究问题和对应的公开数据集的列表如下:
-
视网膜图像分析:
-
从电生理图信号预测足月分娩: TPEHG。
征稿启事
欢迎各领域的专家提交更多测试包,以促进对机器学习性能评分在各研究领域的验证!
结论
对机器学习研究的荟萃分析不包括很多技术,超出了对论文的全面评估和可能尝试重新实现所提出的方法以验证声明结果的范围。mlscorecheck包提供的功能使得对机器学习研究的荟萃分析更为简明、数值化,有助于维护各研究领域的完整性。
进一步阅读
如需更多信息,我们建议查看:
-
mlscorecheck包的 README,
-
套件中提供的说明性笔记本,
-
详细文档,
-
描述数值方法的预印本。
测试支持 1162 种语言的大规模多语言语音(MMS)模型
探索 Meta 最新的自动语音识别(ASR)模型的前沿多语言功能
·发表于 Towards Data Science ·阅读时间 8 分钟·2023 年 5 月 26 日
--
介绍
大规模多语言语音(MMS)¹ 是 Meta AI 最近发布的产品(就在几天前)。它通过将其覆盖范围从大约 100 种语言扩展到超过 1,000 种语言,推动了语音技术的边界。这是通过构建一个单一的多语言语音识别模型实现的。该模型还可以识别超过 4,000 种语言,比以前的能力提高了 40 倍。
MMS 项目旨在让人们更容易以他们首选的语言获取信息和使用设备。它将文本转语音和语音转文本技术扩展到服务不足的语言,继续减少我们全球世界中的语言障碍。现有的应用程序现在可以包括更多种类的语言,例如虚拟助手或语音激活设备。与此同时,跨文化沟通中出现了新的应用场景,例如在消息服务或虚拟与增强现实中。
在本文中,我们将介绍 MMS 在英语和葡萄牙语的 ASR 应用,并提供逐步指南,帮助设置环境以运行该模型。
图 1:大规模多语言语音(MMS)能够识别超过 4,000 种语言,并支持 1162 种语言(source)
本文属于“大语言模型编年史:导航 NLP 前沿”系列文章中的一篇,这是一个新的每周系列文章,旨在探索如何利用大型模型的力量来完成各种 NLP 任务。通过深入了解这些前沿技术,我们旨在赋能开发者、研究人员和爱好者,利用 NLP 的潜力并开启新的可能性。
迄今为止发布的文章:
与往常一样,代码可以在我的 Github 上找到。
构建大规模多语言语音模型的方法
Meta 使用了宗教文本,例如《圣经》,来构建一个覆盖广泛语言的模型。这些文本有几个有趣的特点:首先,它们被翻译成多种语言,其次,有公开的音频录音,记录了不同语言的人朗读这些文本。因此,训练该模型的主要数据集是《新约》,研究团队能够收集到超过 1,100 种语言的文本,并提供了每种语言超过 32 小时的数据。他们进一步拓展了识别 4,000 种语言的能力。这是通过使用各种其他基督教宗教阅读的未标注录音实现的。从实验结果来看,尽管数据来自特定领域,但它具有良好的泛化能力。
这些并不是工作的唯一贡献。他们创建了一个新的预处理和对齐模型,可以处理长时间的录音。这被用来处理音频,错位的数据通过最终的交叉验证过滤步骤被移除。回忆一下我们之前的文章,我们看到 Whisper 的一个挑战是无法正确对齐转录。该方法的另一个重要步骤是使用 wav2vec 2.0,一个自监督学习模型,来在超过 1,400 种语言的海量语音数据(约 500,000 小时)上训练他们的系统。我们之前讨论的标注数据集不足以训练 MMS 这样规模的模型,因此使用了 wav2vec 2.0 来减少对标注数据的需求。最后,结果模型会被微调以执行特定的语音任务,例如多语言语音识别或语言识别。
MMS 模型在几天前由 Meta 开源,并在 Fairseq 仓库中提供。在下一节中,我们将深入了解 Fairseq 及如何测试这些来自 Meta 的新模型。
Fairseq 仓库概览:一个强大的序列到序列学习工具包
Fairseq 是一个开源的序列到序列工具包,由 Facebook AI Research(也称为 FAIR)开发。它提供了各种序列建模算法的参考实现,包括卷积神经网络、递归神经网络、变压器及其他架构。
Fairseq 仓库基于 PyTorch,这是另一个开源项目,最初由 Meta 开发,现在归 Linux 基金会旗下。它是一个非常强大的机器学习框架,提供了高灵活性和速度,特别是在深度学习方面。
Fairseq 实现旨在帮助研究人员和开发者训练自定义模型,支持翻译、摘要、语言建模和其他文本生成任务。Fairseq 的一个关键特性是它支持分布式训练,这意味着它可以有效利用多个 GPU,无论是在单台机器上还是跨多台机器。这使其非常适合大规模机器学习任务。
大规模多语言语音模型
Fairseq 提供了两个可下载的预训练模型:MMS-300M 和 MMS-1B。你还可以访问针对不同语言和数据集的微调模型。为了我们的目的,我们测试了在 FLEURS 数据集中针对 102 种语言微调的 MMS-1B 模型,以及针对 1162 种语言(!)微调的 MMS-1B-all,微调使用了多个不同的数据集。
实现自动语音识别与大规模多语言语音
请记住,这些模型仍处于研究阶段,使得测试变得更具挑战性。与生产就绪的软件相比,还需要额外的步骤。
首先,你需要在项目根目录下设置一个.env
文件,以配置你的环境变量。它应如下所示:
CURRENT_DIR=/path/to/current/dir
AUDIO_SAMPLES_DIR=/path/to/audio_samples
FAIRSEQ_DIR=/path/to/fairseq
VIDEO_FILE=/path/to/video/file
AUDIO_FILE=/path/to/audio/file
RESAMPLED_AUDIO_FILE=/path/to/resampled/audio/file
TMPDIR=/path/to/tmp
PYTHONPATH=.
PREFIX=INFER
HYDRA_FULL_ERROR=1
USER=micro
MODEL=/path/to/fairseq/models_new/mms1b_all.pt
LANG=eng
接下来,你需要配置位于fairseq/examples/mms/asr/config/infer_common.yaml
的 YAML 文件。该文件包含脚本使用的重要设置和参数。
在 YAML 文件中,为checkpoint
字段使用完整路径,如下所示(除非你使用的是容器化应用程序来运行脚本):
checkpoint: /path/to/checkpoint/${env:USER}/${env:PREFIX}/${common_eval.results_path}
这个完整路径是必要的,以避免潜在的权限问题,除非你在容器中运行应用程序。
如果你计划使用 CPU 进行计算而不是 GPU,则需要在 YAML 文件的顶部添加以下指令:
common:
cpu: true
此设置指示脚本使用 CPU 进行计算。
我们使用dotevn
Python 库在 Python 脚本中加载这些环境变量。由于我们正在覆盖一些系统变量,我们需要使用一个技巧以确保加载正确的变量。我们使用dotevn_values
方法并将输出存储在一个变量中。这确保了我们获取的是存储在.env
文件中的变量,而不是即使名称相同的随机系统变量。
config = dotenv_values(".env")
current_dir = config['CURRENT_DIR']
tmp_dir = config['TMPDIR']
fairseq_dir = config['FAIRSEQ_DIR']
video_file = config['VIDEO_FILE']
audio_file = config['AUDIO_FILE']
audio_file_resampled = config['RESAMPLED_AUDIO_FILE']
model_path = config['MODEL']
model_new_dir = config['MODELS_NEW']
lang = config['LANG']
然后,我们可以克隆 fairseq GitHub 仓库并在我们的机器上安装它。
def git_clone(url, path):
"""
Clones a git repository
Parameters:
url (str): The URL of the git repository
path (str): The local path where the git repository will be cloned
"""
if not os.path.exists(path):
Repo.clone_from(url, path)
def install_requirements(requirements):
"""
Installs pip packages
Parameters:
requirements (list): List of packages to install
"""
subprocess.check_call(["pip", "install"] + requirements)
git_clone('https://github.com/facebookresearch/fairseq', 'fairseq')
install_requirements(['--editable', './'])
我们已经讨论了本文中使用的模型,因此让我们将它们下载到本地环境中。
def download_file(url, path):
"""
Downloads a file
Parameters:
url (str): URL of the file to be downloaded
path (str): The path where the file will be saved
"""
subprocess.check_call(["wget", "-P", path, url])
download_file('https://dl.fbaipublicfiles.com/mms/asr/mms1b_fl102.pt', model_new_dir)
关于 MMS 模型的输入,还有一个额外的限制,即音频数据的采样率需要为 16000 Hz。在我们的情况下,我们定义了两种生成这些文件的方法:一种是将视频转换为音频,另一种是重新采样音频文件以获得正确的采样率。
def convert_video_to_audio(video_path, audio_path):
"""
Converts a video file to an audio file
Parameters:
video_path (str): Path to the video file
audio_path (str): Path to the output audio file
"""
subprocess.check_call(["ffmpeg", "-i", video_path, "-ar", "16000", audio_path])
def resample_audio(audio_path, new_audio_path, new_sample_rate):
"""
Resamples an audio file
Parameters:
audio_path (str): Path to the current audio file
new_audio_path (str): Path to the output audio file
new_sample_rate (int): New sample rate in Hz
"""
audio = AudioSegment.from_file(audio_path)
audio = audio.set_frame_rate(new_sample_rate)
audio.export(new_audio_path, format='wav')
我们现在准备使用支持 1162 种语言的 MMS-1B-all 模型运行推理过程。
def run_inference(model, lang, audio):
"""
Runs the MMS ASR inference
Parameters:
model (str): Path to the model file
lang (str): Language of the audio file
audio (str): Path to the audio file
"""
subprocess.check_call(
[
"python",
"examples/mms/asr/infer/mms_infer.py",
"--model",
model,
"--lang",
lang,
"--audio",
audio,
]
)
run_inference(model_path, lang, audio_file_resampled)
使用 Fairseq 的自动语音识别结果
在这一部分,我们描述了我们的实验设置并讨论了结果。我们使用 Fairseq 的两种不同模型(MMS-1B-all 和 MMS-1B-FL102)在英语和葡萄牙语中进行了 ASR。你可以在我的 GitHub 库中找到音频文件。这些文件是我自己生成的,仅用于测试目的。
让我们从 MMS-1B-all 模型开始。以下是英语和葡萄牙语音频样本的输入和输出:
Eng:只需一个小片段即可了解新的 Facebook Research 模型在语音识别任务中的实际表现
Por:现在只需在这里提供一个示例,以便尝试了解 Facebook Research 的新模型是否真正有效,我们将进行测试。
使用 MMS-1B-FL102 时,生成的语音质量显著下降。我们来看一下英语的相同示例:
Eng:只需回顾一下小片段,以了解新的 Facebook Research 模型在速度识别任务中的实际表现,让我们看看
尽管生成的语音对于我们今天拥有的模型标准来说并不特别令人印象深刻,但我们需要从这些模型可以使 ASR 服务于更广泛全球人群的角度来考虑这些结果。
结论
Meta 开发的 Massively Multilingual Speech 模型代表了推动全球沟通和扩大语言技术应用范围的又一步。它能够理解超过 4000 种语言,并在其中 1162 种语言中有效运行,提高了许多传统上被忽视语言的可及性。
我们对 MMS 模型的测试展示了该技术在当前阶段的可能性和局限性。尽管 MMS-1B-FL102 模型生成的语音不如预期令人印象深刻,但 MMS-1B-all 模型提供了有希望的结果,展示了其在英语和葡萄牙语中的转录能力。葡萄牙语一直是那些服务不足的语言之一,特别是当我们考虑到来自葡萄牙的葡萄牙语时。
欢迎尝试你喜欢的语言,并在评论区分享转录结果和反馈。
保持联系:LinkedIn
参考文献
文本分类挑战:极小数据集上的微调与 ChatGPT
LLM 在极小数据集上表现优秀,但随着数据集的增大,传统方法则表现突出
·
关注 发布于 Towards Data Science · 7 分钟阅读 · 2023 年 7 月 7 日
--
Debby Hudson拍摄的照片,发布于Unsplash。
Toloka ML 团队不断研究并比较在各种条件下的文本分类不同方法。在这里,我们展示了一个关于 NLP 模型在极小数据集上训练表现的实验。
之前,我们提供了一个关于潜在解决方案的简要概述和将经典模型与大型语言模型(LLMs)进行比较的内容,针对特定的文本分类任务。然而,那些比较是基于一个包含足够数据点以建立可靠分类器的“常规”数据集。在实际应用场景中,你可能会遇到数据有限或没有进行人工标注的情况。
直观上,像 GPT-3 或 ChatGPT 这样的 LLMs 可能会由于其广泛的“知识”而优于较小的模型。为了验证这一假设,我们通过提取较大数据集的一部分创建了一个人工的小数据集,并比较了几种方法。我们微调了 RoBERTa 基础模型,使用 ChatGPT 进行少量示例分类,并微调了 GPT-3 Babbage 模型。
数据集
为了评估各种模型的理解能力,我们选择了一个由科学文章摘要组成的多类别数据集。任务是确定每篇文章的领域。
我们选择了WOS-11967 [1] 数据集,该数据集包含 11,967 个文档,涵盖 35 个类别,其中包括七个父类别:医学、心理学、计算机科学、生物化学、电气工程、土木科学和机械工程。我们抽取了 10,000 个数据点,仅关注父类别进行分析。
尽管数据集并未完全平衡,但类别分布相当合理。因此,所有类别都可能取得令人满意的结果。类别分布如下图所示。
WOS-11967数据集的样本类别分布
通过人工分析,我们发现确定一些摘要的领域相对简单,而在其他情况下,任务变得更加具有挑战性。例如,计算机科学文章可能讨论数学主题,或心理学文章可能包含医学或生化术语和缩写,使其难以与生物化学或医学领域区分开。摘要的长度也有显著差异,平均为 274 个 token(ChatGPT tokens),标准差为 115 个 token。
为了模拟涉及超小数据集的场景,我们对语料库进行了训练-测试拆分,并将少量样本分配到训练集中。我们重复进行了三次不同训练集大小的测试,以评估模型基于可用训练数据的性能变化。我们为实验创建了三个拆分:WOS-11967-s200(训练集中 200 个样本,测试集中 9,800 个样本),WOS-11967-s500(500 / 9,500),和 WOS-11967-s2000(2,000 / 8,000)。
现在,让我们看看使用不同模型解决这些问题所获得的结果。
正常的 RoBERTa 微调
作为基线,我们选择了RoBERTa base模型,并在前面提到的三个数据集上进行了微调。我们对每次运行使用了相同的超参数配置(批量大小为 32,学习率为 3e-5,带热身的线性调度器,256 标记窗口),并采用了早停法以防止过拟合。
我们获得了以下结果:
数据显示,200 个样本不足以提取准确分类摘要所需的所有模式和信息。较低的宏平均 F1 得分也表明模型在机械工程等代表性不足的类别上表现不佳。这表明,仅有少量来自特定类别的样本是不够的。
正如预期的那样,随着可用数据量的增加,模型的性能得到了改善——最终实现了在七个类别中的多类别分类的强大性能。
与 ChatGPT 的少样本学习
我们探索的第二种方法是使用 ChatGPT 的少样本分类。这种方法与传统分类方法有很大不同,因为它不涉及训练模型本身。相反,我们设计了输入提示以实现最佳性能。
然而,由于模型的 4096 标记上下文大小限制,无法将所有 200 个样本输入模型。根据上述测量,我们只能向模型展示大约 14 个摘要。在考虑到用于指令和分隔符的标记时,这一数字进一步减少。
最初,我们采用了“系统”角色来提供指令,并为每个类别提供了一个示例以指导模型的响应。我们将类别名称简化为单个标记,同时保留其含义。这使得模型更容易选择合适的类别,并将输出限制为单个标记。例如,“生物化学”变成了“Bio”,“计算机科学”变成了“Computer”。此外,我们通过提供类别列表让模型选择,并指示模型如果不确定类别则返回“Unknown”标记,从而限制了生成的标记数量。
总体而言,使用这种方法的性能不如仅在 200 个样本上训练的 RoBERTa 模型。我们注意到模型的分类能力严重依赖于提供的提示。修改一句话可能会改善或恶化指标。在某些情况下,尽管有明确指示,ChatGPT 仍错过了类别(这可能是我们制定提示方式的一个缺陷)。
在一些边缘情况下,它生成了未列在指令中的类别,但描述了文章领域,如“数学”或“化学”。尚不清楚这些缺陷应归因于模型还是数据集。然而,根据验证集,这些类别可以使用简单规则进行更正,如将所有“数学”实例改为“计算机”。
为了提高指标,我们尽可能使用了更多的数据。由于我们仍无法将全部 200 个样本输入模型,我们设计了一个两阶段的过程:
-
首先,我们要求模型识别特定领域摘要之间的相似性并生成总结。
-
其次,我们将这些总结纳入指令中,以便为模型提供关于第一阶段模型自身识别的类别和特征的见解。
这种方法使我们能够将更多训练数据样本输入模型;并且有效——我们将指标提升了约 10%。以下是我们用来生成这些总结的提示:
用于提取有关文章领域的有意义信息的 ChatGPT 提示
对于每个领域,我们提供了七到八个摘要,共使用了 63 个不同的摘要来准备分类提示(每七个类别八个摘要用于构建总结,七个摘要作为实际提示中的示例)。
尽管如此,我们指示模型在对类别不确定时回复“未知”。在验证集中,我们观察到大多数“未知”回应对应于计算机科学文章。然后我们用“计算机”类别替换了所有“未知”实例。
生成的分类提示如下:
用于分类文章摘要的最终提示
再次强调,性能受提示和提供的样本的影响很大。模型还生成了多个目标列表之外的类别,需要根据验证集进行手动调整。这种方法得出了以下结果:
性能明显优于在 200 个样本上微调的 RoBERTa 模型——并且所需样本更少。然而,随着标记数据的增加,RoBERTa 开始超过这种方法,即使只有 500 个样本。
我们相信,通过适当的提示工程,可以进一步提升性能。一些有用的提示和技巧可以在Prompting Guide中找到。
微调 GPT-3 模型
对于我们最终的方法,我们在这三个数据集上对 GPT-3 Babbage 模型进行了微调。我们遵循了OpenAI 指南中概述的数据集准备建议,并选择了默认的超参数,没有进行任何特定的调整。每个数据集的训练过程大约花费了 20 分钟,得到以下结果:
微调后的 GPT-3 模型即使在最小的数据集上也表现出色,超越了 RoBERTa 和 ChatGPT。随着训练数据量的增加,RoBERTa 和调优后的 GPT-3 模型之间的性能差距缩小。这引发了对使用这两种选项的资源和可行性的疑问。我们在之前的文章中讨论了这两种方法的优缺点。
结论
这个实验表明,我们的初步假设是正确的——在更大数据上训练的更大模型在额外小的数据集上表现显著更好。通过适当的提示工程和少样本技术,可以取得良好的结果。
然而,随着数据集大小的增加,性能差异会减少。此外,适当定制的经典模型,例如领域适配的 RoBERTa 模型,有时可以在分类任务中优于通用的 LLM。这可以归因于模型对特定主题的专门“知识”。此外,通过正确的优化,使用这些模型的推断速度可以显著提高,这在开发在线服务时至关重要。
除非另有说明,所有图片均由作者提供。
来源
-
Kowsari K, Brown DE, Heidarysafa M, Jafari Meimandi K, Gerber MS, Barnes LE. HDLTex: 分层深度学习用于文本分类。载于:机器学习与应用(ICMLA),2017 年第 16 届 IEEE 国际会议。IEEE; 2017。
-
刘 Y, Ott M, Goyal N 等. RoBERTa: 一种稳健优化的 BERT 预训练方法。CoRR。2019;abs/1907.11692.
arxiv.org/abs/1907.11692
使用 Transformer 编码器进行文本分类
原文:
towardsdatascience.com/text-classification-with-transformer-encoders-1dcaa50dabae
使用 Transformer 编码器进行文本分类的逐步说明
·发表于 Towards Data Science ·阅读时间 15 分钟·2023 年 8 月 11 日
--
毋庸置疑,Transformer 是深度学习领域最重要的突破之一。该模型的编码器-解码器架构在跨领域应用中证明了其强大功能。
最初,Transformer 仅用于语言建模任务,如机器翻译、文本生成、文本分类、问答等。然而,最近 Transformer 也被用于计算机视觉任务,如图像分类、物体检测和语义分割。
鉴于其受欢迎程度以及存在许多基于 Transformer 的复杂模型,如 BERT、Vision-Transformer、Swin-Transformer 和 GPT 家族,我们必须深入了解 Transformer 架构的内部工作原理。
在本文中,我们将仅分析 Transformer 的编码器部分,该部分主要用于分类目的。具体来说,我们将使用 Transformer 编码器来分类文本。事不宜迟,让我们首先查看一下我们将在本文中使用的数据集。
关于数据集
我们将使用的数据集是电子邮件数据集。您可以通过这个 链接 在 Kaggle 上下载此数据集。该数据集的许可证为 CC0:公共领域,这意味着您可以自由使用和分发此数据集。
import math
import torch
import torch.nn as nn
import torchtext
import pandas as pd
from sklearn.model_selection import train_test_split
from torch.utils.data import DataLoader
from tqdm import tqdm
from torchtext.data.utils import get_tokenizer
from torchtext.vocab import build_vocab_from_iterator
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
df = pd.read_csv('spam_ham.csv')
df_train, df_test = train_test_split(df, test_size=0.2, random_state=42)
print(df_train.head())
# Output
'''
Category Message
1978 spam Reply to win £100 weekly! Where will the 2006 ...
3989 ham Hello. Sort of out in town already. That . So ...
3935 ham How come guoyang go n tell her? Then u told her?
4078 ham Hey sathya till now we dint meet not even a si...
4086 spam Orange brings you ringtones from all time Char...
'''
任务非常简单:这是一个二分类问题,给定一封电子邮件的文本,我们的 Transformer 编码器模型需要预测该文本是否为垃圾邮件。
接下来,我们创建一个从标签到其索引的映射,例如 ‘ham’ 将是 0,而 ‘spam’ 将是 1。
labels = df_train["Category"].unique()
num_labels = len(labels)
label2id, id2label = dict(), dict()
for i, label in enumerate(labels):
label2id[label] = i
id2label[i] = label
print(id2label)
print(label2id)
# Output
'''
{0: 'spam', 1: 'ham'}
{'spam': 0, 'ham': 1}
'''
现在让我们了解 Transformer 编码器模型的整体工作流程。
Transformer 编码器如何工作
要理解 Transformer 编码器如何工作,我们从过程的最开始部分开始,即数据预处理。
正如你所知道的,我们将在本文中处理文本数据,而 Transformer 无法处理原始文本。因此,我们首先要做的就是将文本转换为机器可读的格式,这可以通过标记化过程来实现。
标记化
标记化是将输入文本拆分为标记的过程。一个标记可以由一个字符、一个单词或一个子词组成,这取决于使用的标记化器类型。在这篇文章中,我们将使用单词级标记化,这意味着每个标记代表一个单词。
# Load tokenizer
tokenizer = get_tokenizer('basic_english')
text = 'this is text'
print(tokenizer(text))
# Output
'''
[this, is, text]
'''
接下来,每个标记将根据所谓的词汇表映射到其整数表示。
词汇表基本上是字符、单词或子词及其整数映射的集合。由于我们在单词级别进行标记化,因此我们的词汇表将是单词及其整数映射的集合。
让我们基于训练数据集建立一个词汇表:
# Initialize training data iterator
class TextIter(torch.utils.data.Dataset):
def __init__(self, input_data):
self.text = input_data['Message'].values.tolist()
def __len__(self):
return len(self.text)
def __getitem__(self, idx):
return self.text[idx]
# Build vocabulary
def yield_tokens(data_iter):
for text in data_iter:
yield tokenizer(text)
data_iter = TextIter(df_train)
vocab = build_vocab_from_iterator(yield_tokens(data_iter), specials=["<pad>", "<unk>"])
vocab.set_default_index(vocab["<unk>"])
print(vocab.get_stoi())
# Output
'''
{'<pad>':0, '<unk>':1,..., 'ny-usa': 7449, ...}
'''
如上面的代码片段所示,我们训练数据中的每个单词在词汇表中都有其独特的整数。如果你注意到,我们还将两个特殊标记
与此同时,
text_unk = 'this is jkjkj' # jkjkj is an unknown word in our vocab
seq_unk = [vocab[word] for word in tokenizer(text_unk)]
print(tokenizer(text_unk))
print(seq_unk)
# Output
'''
['this', 'is', 'jkjkj']
[49, 15, 1]
'''
现在让我们创建一个玩具示例,贯穿整个文章。
# We will use this example throughout the article
text = 'this is text'
seq = [vocab[word] for word in tokenizer(text)]
print(tokenizer(text))
print(seq)
# Output
'''
['this', 'is', 'text']
[49, 15, 81]
'''
嵌入层
每个标记的整数表示就是我们传递给 Transformer 编码器模型第一层的输入,这一层是嵌入层。该层将每个整数转换为我们预先设置维度的向量。
class Embeddings(nn.Module):
def __init__(self, d_model, vocab_size):
super(Embeddings, self).__init__()
self.emb = nn.Embedding(vocab_size, d_model)
self.d_model = d_model
def forward(self, x):
return self.emb(x) * math.sqrt(self.d_model)
每个向量的维度通常对应于我们为 Transformer 模型选择的隐藏层大小。例如,BERT-base 模型的隐藏层大小为 768。
在以下示例中,我们序列中的每个标记 ([‘this’, ‘is’, ‘text’]) 将被转换为 4D 向量嵌入。
hidden_size = 4
input_data = torch.LongTensor(seq).unsqueeze(0)
emb_model = Embeddings(hidden_size, len(vocab))
token_emb = emb_model(input_data)
print(f'Size of token embedding: {token_emb.size()}')
# Output
'''
Size of token embedding: torch.Size([1, 3, 4]) [batch, no. seq token, dim]
'''
嵌入层的输出是一个 [batch, sequence_length, embedding_dim]
的张量。
作者提供的图片
位置编码
到目前为止,我们已经获得了序列中每个标记的嵌入,但这些嵌入并没有顺序感。同时,我们知道在任何文本和语言中,词语的顺序对捕捉句子的语义意义至关重要。
为了捕捉输入序列的顺序,Transformer 应用了一种叫做位置编码的方法。我们可以使用多种方式来应用位置编码,但它应满足以下条件:
-
编码应该对序列中的每个标记都是唯一的。
-
任意两个相邻标记之间的delta值或距离应该是一致的,并且与序列长度无关。
-
编码应该是确定性的。
-
并且它在我们处理更长序列时也应表现出良好的泛化能力。
在原始 Transformer 论文中,作者提出了一种利用正弦和余弦波组合的位置信息编码方法。这种方法满足了所有提到的条件,使模型能够有效地捕捉标记的顺序。
作者提供的图像
class PositionalEncoding(nn.Module):
def __init__(self, d_model, vocab_size=5000, dropout=0.1):
super().__init__()
self.dropout = nn.Dropout(p=dropout)
pe = torch.zeros(vocab_size, d_model)
position = torch.arange(0, vocab_size, dtype=torch.float).unsqueeze(1)
div_term = torch.exp(
torch.arange(0, d_model, 2).float()
* (-math.log(10000.0) / d_model)
)
pe[:, 0::2] = torch.sin(position * div_term)
pe[:, 1::2] = torch.cos(position * div_term)
pe = pe.unsqueeze(0)
self.register_buffer("pe", pe)
def forward(self, x):
x = x + self.pe[:, : x.size(1), :]
return self.dropout(x)
位置编码应该具有与标记嵌入相同的维度,以便我们可以将位置编码添加到标记嵌入中。此外,位置编码是固定的,这意味着在训练过程中没有可更新的可学习参数。
作者提供的图像
pe_model = PositionalEncoding(d_model=4, vocab_size=len(vocab))
output_pe = pe_model(token_emb)
print(f'Size of output embedding: {output_pe.size()}')
# Output
'''
Size of output embedding: torch.Size([1, 3, 4]) [batch, no. seq token, dim]
'''
从标记嵌入和位置编码的加法中得到的输出嵌入将成为下一步骤的输入,即 Transformer 编码器堆栈。
自注意力
Transformer 编码器堆栈由几个部分组成,如下图所示:
作者提供的图像
首先,我们的输入嵌入将进入所谓的自注意力层。这一层是 Transformer 语言模型能够区分每个词的上下文和整个序列/句子的语义意义的主要因素。
自注意力层将使用不同的线性层将输入嵌入投影到查询、键和值向量中。查询、键和值是我们通常在检索系统或推荐系统中找到的术语。
例如,假设你想在 Netflix 上观看特定的电影。查询将是你在搜索栏中输入的电影标题;键将是 Netflix 目录中每部电影的描述;值将是基于你之前在搜索栏中输入的电影标题的电影推荐结果。
作者提供的图像
正如上面的可视化所示,查询、键和值都来自同一来源。这就是为什么这种注意力机制被称为自注意力。
如果你使用完整的 Transformer 架构(包括解码器部分)来进行像机器翻译这样的自回归任务,那么还会有另一个称为交叉注意力的注意力机制,其中查询来自解码器,而键和值来自编码器堆栈。然而,由于我们只使用编码器堆栈,所以在本文中不会讨论交叉注意力机制。
在获取查询、键和值之后,我们就可以执行自注意力机制了。
首先,我们将查询与键进行相乘(也称为点积操作)。
作者提供的图片
我们从点积操作中得到的是一个方形的注意力矩阵,其尺寸在两个维度上都等于序列中输入标记的数量。该矩阵指示了每个标记应该给序列中其他标记的注意力或相关性。
接下来,我们使用线性层的维度对注意力矩阵进行归一化,以在训练过程中获得稳定的梯度。然后,我们使用 Softmax 函数对矩阵进行归一化,使矩阵中每一行的值都为正且总和为 1。
作者提供的图片
自注意力机制的最后一步是值与我们的归一化注意力矩阵之间的另一个点积。这将给我们一个大小为[batch, no_of_sequence, hidden_size_dim]
的最终输出。
class SingleHeadAttention(nn.Module):
def __init__(self, d_model, d_head_size):
super().__init__()
self.lin_key = nn.Linear(d_model, d_head_size, bias=False)
self.lin_query = nn.Linear(d_model, d_head_size, bias=False)
self.lin_value = nn.Linear(d_model, d_head_size, bias=False)
self.d_model = d_model
def forward(self, x):
query = self.lin_query(x)
key = self.lin_key(x)
value = self.lin_value(x)
scores = torch.matmul(query, key.transpose(-2, -1)) / math.sqrt(self.d_model)
p_attn = scores.softmax(dim=-1)
x = torch.matmul(p_attn, value)
return x
多头注意力
然而,Transformer 模型不仅仅使用一个自注意力块,或通常称为“头”。它使用多头注意力,其中多个单自注意力并行进行。小的区别在于,我们需要将每个单头注意力中的三个线性层的输出除以我们使用的头的总数。这确保了多头注意力的计算时间与单自注意力相当。
作者提供的图片
最后,我们需要将每个自注意力层的输出连接起来,然后将其投影到一个额外的线性层中。
作者提供的图片
class MultiHeadAttention(nn.Module):
def __init__(self, h, d_model, dropout=0.1):
super().__init__()
assert d_model % h == 0
d_k = d_model // h
self.multi_head = nn.ModuleList([SingleHeadAttention(d_model, d_k) for _ in range(h)])
self.lin_agg = nn.Linear(d_model, d_model)
def forward(self, x):
x = torch.cat([head(x) for head in self.multi_head], dim=-1)
return self.lin_agg(x)
就这样。这个多头注意力层的输出张量与输入的维度相同。
mult_att = MultiHeadAttention(h=2, d_model=4)
output_mult_att = mult_att(output_pe)
print(f'Size of output embedding after multi-head attention: {output_mult_att.size()}')
# Output
'''
Size of output embedding after multi-head attention: torch.Size([1, 3, 4])
'''
归一化层和残差连接
如果我们查看 Transformer 编码器块的架构,我们需要将多头注意力的输出与多头注意力的输入(也称为残差连接)相加,然后对其进行归一化。
作者提供的图片
这两个操作的原因是为了使 Transformer 模型在训练过程中能够更快收敛,并且它们还可以帮助模型更准确地进行预测。
class LayerNorm(nn.Module):
def __init__(self, d_model, eps=1e-6):
super(LayerNorm, self).__init__()
self.a_2 = nn.Parameter(torch.ones(d_model))
self.b_2 = nn.Parameter(torch.zeros(d_model))
self.eps = eps
def forward(self, x):
mean = x.mean(-1, keepdim=True)
std = x.std(-1, keepdim=True)
return self.a_2 * (x - mean) / (std + self.eps) + self.b_2
class ResidualConnection(nn.Module):
def __init__(self, d_model, dropout=0.1):
super().__init__()
self.norm = LayerNorm(d_model)
self.dropout = nn.Dropout(dropout)
def forward(self, x1, x2):
return self.dropout(self.norm(x1 + x2))
再次强调,残差连接和归一化层后的输出张量维度将与多头注意力层的输出张量维度相同。
res_conn_1 = ResidualConnection(d_model=4)
output_res_conn_1 = res_conn_1(output_pe, output_mult_att)
print(f'Size of output embedding after residual connection: {output_res_conn_1.size()}')
# Output
'''
Size of output embedding after residual connection: torch.Size([1, 3, 4])
'''
前馈层
残差连接和归一化层的输出将成为前馈层的输入。这个层只是一个普通的线性层,如下所示:
class FeedForward(nn.Module):
def __init__(self, d_model, d_ff, dropout=0.1):
super().__init__()
self.w_1 = nn.Linear(d_model, d_ff)
self.w_2 = nn.Linear(d_ff, d_model)
self.dropout = nn.Dropout(dropout)
def forward(self, x):
return self.w_2(self.dropout(self.w_1(x).relu()))
这一层也不会改变我们张量的维度。
ff = FeedForward(d_model=4, d_ff=12)
output_ff = ff(output_res_conn_1)
print(f'Size of output embedding after feed-forward network: {output_ff.size()}')
# Output
'''
Size of output embedding after feed-forward network: torch.Size([1, 3, 4])
'''
在前馈层之后,我们需要应用第二个残差连接,将前馈层的输出与前馈层的输入相加。加法之后,我们使用上面描述的归一化层对张量进行归一化。
res_conn_2 = ResidualConnection(d_model=4)
output_res_conn_2 = res_conn_2(output_res_conn_1, output_ff)
print(f'Size of output embedding after second residual: {output_res_conn_2.size()}')
# Output
'''
Size of output embedding after second residual: torch.Size([1, 3, 4])
'''
Transformer 编码器堆栈
从多头自注意力层到前馈层之后的归一化层的过程对应一个单独的 Transformer 编码器堆栈。
作者提供的图片
现在我们可以将上述所有过程封装在一个名为 SingleEncoder()
的类中:
class SingleEncoder(nn.Module):
def __init__(self, d_model, self_attn, feed_forward, dropout):
super().__init__()
self.self_attn = self_attn
self.feed_forward = feed_forward
self.res_1 = ResidualConnection(d_model, dropout)
self.res_2 = ResidualConnection(d_model, dropout)
self.d_model = d_model
def forward(self, x):
x_attn = self.self_attn(x)
x_res_1 = self.res_1(x, x_attn)
x_ff = self.feed_forward(x_res_1)
x_res_2 = self.res_2(x_res_1, x_ff)
return x_res_2
在实际应用中,我们通常使用多个 Transformer 编码器,而不是仅使用一个。例如,BERT-base 模型使用了 12 个 Transformer 编码器堆栈。
class EncoderBlocks(nn.Module):
def __init__(self, layer, N):
super().__init__()
self.layers = nn.ModuleList([layer for _ in range(N)])
self.norm = LayerNorm(layer.d_model)
def forward(self, x):
for layer in self.layers:
x = layer(x)
return self.norm(x)
使用上述 EncoderBlocks()
,我们可以根据需要初始化多个 Transformer 编码器堆栈。
模型训练
现在我们知道了 Transformer 编码器的内部结构,接下来让我们使用它来训练数据以进行文本分类。
模型定义
在本文中,我们将使用六个 Transformer 编码器堆栈。隐藏层大小为 300,多头自注意力层中将有四个不同的头。你可以根据自己的需要调整这些值。
class TransformerEncoderModel(nn.Module):
def __init__(self, vocab_size, d_model, nhead, d_ff, N,
dropout=0.1):
super().__init__()
assert d_model % nhead == 0, "nheads must divide evenly into d_model"
self.emb = Embeddings(d_model, vocab_size)
self.pos_encoder = PositionalEncoding(d_model=d_model, vocab_size=vocab_size)
attn = MultiHeadAttention(nhead, d_model)
ff = FeedForward(d_model, d_ff, dropout)
self.transformer_encoder = EncoderBlocks(SingleEncoder(d_model, attn, ff, dropout), N)
self.classifier = nn.Linear(d_model, 2)
self.d_model = d_model
def forward(self, x):
x = self.emb(x) * math.sqrt(self.d_model)
x = self.pos_encoder(x)
x = self.transformer_encoder(x)
x = x.mean(dim=1)
x = self.classifier(x)
return x
model = TransformerEncoderModel(len(vocab), d_model=300, nhead=4, d_ff=50,
N=6, dropout=0.1).to(device)
如果你注意到,我们在最后一个 Transformer 编码器堆栈的输出上添加了一个额外的线性层。这个线性层将作为分类器。由于我们只有两个不同的类别(垃圾邮件/正常邮件),因此这个线性层的输出将是二。
另外,我们需要解决的一个重要问题是,最终堆栈的输出将是[batch, no_of_sequence, hidden_size]
,而我们的最终线性层期望的输入是[batch, hidden_size]
。我们可以采用几种方法来使堆栈的输出与线性层的输入匹配。
例如,BERT 只使用了一个特殊的称为 [CLS] 的标记,该标记在 Transformer 架构中的位置编码步骤之前插入到序列中。在这里,我们没有这个特殊的 [CLS] 标记。因此,我们改为在最后一个编码器堆栈之后对所有输出嵌入值进行平均。
数据加载器
接下来,我们需要为训练数据创建一个数据加载器,以便在训练过程中将数据分批输入到模型中。
class TextDataset(torch.utils.data.Dataset):
def __init__(self, input_data):
self.text = input_data['Message'].values.tolist()
self.label = [int(label2id[i]) for i in input_data['Category'].values.tolist()]
def __len__(self):
return len(self.label)
def get_sequence_token(self, idx):
sequence = [vocab[word] for word in tokenizer(self.text[idx])]
len_seq = len(sequence)
return sequence, len_seq
def get_labels(self, idx):
return self.label[idx]
def __getitem__(self, idx):
sequence, len_seq = self.get_sequence_token(idx)
label = self.get_labels(idx)
return sequence, label, len_seq
def collate_fn(batch):
sequences, labels, lengths = zip(*batch)
max_len = max(lengths)
for i in range(len(batch)):
if len(sequences[i]) != max_len:
for j in range(len(sequences[i]),max_len):
sequences[i].append(0)
return torch.tensor(sequences, dtype=torch.long), torch.tensor(labels, dtype=torch.long)
除了 dataloader 类,我们还需要创建上述的辅助函数 collate_fn()
。这个函数是必不可少的,因为为了将训练数据按批次提供,每个批次需要具有相同的维度。
由于我们处理的是具有不同句子长度的文本数据,因此每个批次的维度不一定相同。在 collate_fn
中,我们首先获取批次中序列的最大长度,然后向较短的序列中添加一堆
另一种你可以使用的方法是通过定义最大标记数。接下来,如果句子的标记数超过最大值,可以截断句子;如果标记数少于最大值,则可以添加一堆
训练循环
现在我们已经定义了模型架构和数据加载器类,那么我们可以开始训练模型了。
def train(model, dataset, epochs, lr, bs):
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam((p for p in model.parameters()
if p.requires_grad), lr=lr)
train_dataset = TextDataset(dataset)
train_dataloader = DataLoader(train_dataset, num_workers=1, batch_size=bs, collate_fn=collate_fn, shuffle=True)
# Training loop
for epoch in range(epochs):
total_loss_train = 0
total_acc_train = 0
for train_sequence, train_label in tqdm(train_dataloader):
# Model prediction
predictions = model(train_sequence.to(device))
labels = train_label.to(device)
loss = criterion(predictions, labels)
# Calculate accuracy and loss per batch
correct = predictions.argmax(axis=1) == labels
acc = correct.sum().item() / correct.size(0)
total_acc_train += correct.sum().item()
total_loss_train += loss.item()
# Backprop
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), 0.5)
optimizer.step()
print(f'Epochs: {epoch + 1} | Loss: {total_loss_train / len(train_dataset): .3f} | Accuracy: {total_acc_train / len(train_dataset): .3f}')
epochs = 15
lr = 1e-4
batch_size = 4
train(model, df_train, epochs, lr, batch_size)
你将得到类似这样的输出:
图片由作者提供
模型预测
在我们训练模型之后,我们可以自然地使用它来预测测试集中的未见数据。为此,我们首先需要创建一个函数,封装数据预处理步骤和模型预测步骤。
def predict(text):
sequence = torch.tensor([vocab[word] for word in tokenizer(text)], dtype=torch.long).unsqueeze(0)
output = model(sequence.to(device))
prediction = id2label[output.argmax(axis=1).item()]
return prediction
现在,如果我们想预测测试集中的文本,可以直接调用上述函数:
idx = 24
text = df_test['Message'].values.tolist()[idx]
gt = df_test['Category'].values.tolist()[idx]
prediction = predict(text)
print(f'Text: {text}')
print(f'Ground Truth: {gt}')
print(f'Prediction: {prediction}')
# Output
'''
Text: This is the 2nd time we have tried 2 contact u. U have won the £750 Pound prize. 2 claim is easy, call 087187272008 NOW1! Only 10p per minute. BT-national-rate.
Ground Truth: spam
Prediction: spam
'''
idx = 35
text = df_test['Message'].values.tolist()[idx]
gt = df_test['Category'].values.tolist()[idx]
prediction = predict(text)
print(f'Text: {text}')
print(f'Ground Truth: {gt}')
print(f'Prediction: {prediction}')
# Output
'''
Text: Morning only i can ok.
Ground Truth: ham
Prediction: ham
'''
结论
在这篇文章中,我们讨论了利用 Transformer 的编码器部分来分类文本的逐步过程。如你所知,许多大型语言模型使用 Transformer 的编码器部分。例如,BERT 由于其 Transformer 编码器架构结合了大量的训练数据,在许多语言任务中取得了最先进的表现。
我希望这篇文章能帮助你入门 Transformer 架构。像往常一样,你可以通过 这个笔记本 查找本文实现的代码。
使用 NLP 进行文本纠正
原文:
towardsdatascience.com/text-correction-using-nlp-b68c7233b86
检测和纠正常见错误:问题与方法
·发布在 Towards Data Science ·19 分钟阅读·2023 年 1 月 13 日
--
图片由 Lorenzo Cafaro 提供,来源于 Pixabay
任何写作的人都会不时漏掉一个逗号。或者在某种语境下使用错误的介词。或者拼写错误。或者措辞尴尬。或者使用过于复杂或过长的句子。或者段落过长。或者过于冗长。
对于除了最短的写作之外的所有文本,也许以上所有内容以及更多。
我曾经有一个学生在他写的每一篇文章中都缺少了像a或the这样的冠词。我阅读了他数百页的内容,却没有找到一个冠词。
我曾经并且仍然不断地犯这些错误。即使是在我的短文如电子邮件中。
对于内容较为复杂的文本,如整本书籍或甚至短篇博客,文本问题自然会更多。这就是我们需要校对编辑的原因,他们的职责包括校对和编辑内容。
这也是为什么基于 NLP 的工具如 Grammarly 越来越受欢迎的原因。这些工具可以在几分钟内帮助人们在短文本如电子邮件中发现并纠正这些错误。对于较长的文本,它们可能会发现更多错误,这当然意味着修正这些错误会花费更多时间。
有关 Grammarly 的更多信息,请参见 [10]。
无论如何,作家无法与这些工具在提高高质量写作产量方面竞争。我还要补充一点,这样可以减少眼睛的疲劳。
这让我想起了以下的事情。很久很久以前,我写了一篇博士论文。数百页。那是一个痛苦的过程。如果没有像 Grammarly 这样的工具,我现在不会重复这个过程。
前言
在这篇文章中,我们首先描述并解释人们在写作时倾向于犯的各种错误。我们将仅限于基本错误,例如缺少逗号、缺少冠词或使用错误的介词。(在这篇文章中,我们用“错误”这个术语时比较宽松。我们实际上是指“改进建议”。)
随后,我们将集思广益,使用自然语言处理技术检测这些问题,并提供解决方案。
我们选择将这篇文章的范围限制在基本错误的原因有几个。首先,它们非常常见。其次,一些统计自然语言处理的基本方法,再加上一些特征工程,适用于这些错误的检测和纠正。
因此,在这个过程中,读者还将获得在有用且真实的环境中对统计自然语言处理基本方法的坚实基础。
相比之下,检测并提供针对更复杂问题的纠正建议,如措辞尴尬或以更简洁、更易读的方式重新表达,需要更先进的自然语言处理技术。在另一篇文章[11]中,我们也建模了上下文,超出了这篇文章所涵盖的基本问题。这仍然没有涵盖措辞尴尬或更简洁、更具信息性的表达,因为这些都是更复杂的话题。
文本中的基本错误
这里是我们将涵盖的内容,并附有实际示例。
-
缺少逗号。
-
缺少冠词。
-
忘记在“It's”中使用撇号。
-
使用单数而非复数,或反之亦然。
-
在不应该使用连字符的地方使用了连字符,或反之亦然。
-
大小写问题:未在应大写字母的地方大写; 在应全大写的地方没有全大写。例如,fyi。
-
使用错误的介词,或在不需要时使用介词。
接下来,我们将对这些错误进行第二次讨论,探讨统计自然语言处理的基本解决方案。这一讨论将涉及使用哪些训练集、提取哪些特征,以及使用哪些统计模型。
我们甚至不会尝试解决上述以斜体标出的那些问题。这些问题需要更先进的自然语言处理方法,即那些能建模上下文的方法。
缺少逗号
我犯的最常见的错误是缺少逗号。考虑一下:
Regardless there is no way …
Clearly the writer meant …
在每种情况下,第一个单词后面都应该立即跟一个逗号。
大小写问题
有时会忘记在句子的开头单词中大写第一个字母,或在指代自己时大写‘i’。例如:
the most frequent error i make is …
这会给人留下不好的印象。
同样,
An fyi in case you are interested.
更好的表达方式是:
An FYI in case you are interested.
还有许多其他情况,其中全大写字母的形式更为适合。例如pdf、gpx、cdc、nlp、ai等。
缺少冠词
遗漏冠词也很常见。如:
… within matter of minutes … ⇒
… within a matter of minutes …
… capitalize first letter in word … ⇒
… capitalize the first letter in the word …
⇒之前的文本是原文。⇒之后的文本是正确的写法。我们将在整篇文章中使用这种约定来展示示例。
另一个我经常犯的错误是省略了Its中的撇号。实际上,我在本节开始的Its上犯了这个错误!
错误使用连字符
我经常犯这样的错误。以下是来自一个不错的帖子中的一些例子[2]。
在这个阶段,我只是展示这些例子。稍后当我们查看方法时,我们会引入一些[2]中的额外点。它们将帮助我们设计正确的特征或决定我们应该使用统计 NLP 中的哪种方法。
He’s an all star runner ⇒
He’s an all-star runner
Chocolate covered truffles ⇒
Chocolate-covered truffles
以及这些。
My co-worker has a four-year old child. ⇒
My co-worker has a four-year-old child
We sat on the plane for deicing for an hour. ⇒
We sat on the plane for de-icing for an hour.
让我们看看一些相反方向的例子。在这些例子中,我们不应使用连字符。
This car is a finely-tuned machine. ⇒
This car is a finely tuned machine.
She attends Ohio-State University. ⇒
She attends Ohio State University.
Heart-broken ⇒
Heartbroken
介词使用错误
我经常犯涉及介词的错误。特别是,我使用了错误的介词。例如,我经常使用by而应使用with。
如果我没有使用 Grammarly,我甚至不会知道自己在做这些事。即使在审阅我的文本之后也是如此。
以下是我想分享的第一个例子。它不是特定于介词的,但确实说明了我想表达的观点。
在写上述句子时,我写成了
And were I to not be using Grammarly, …
Grammarly 建议我去掉to和be。
And were I not using Grammarly
更容易阅读。
哦,实际上,我刚意识到在上述几行中还有两个额外的错误。现在已经修正。以下是包含错误的版本。
Below is the first example I want share.
Its Grammarly that …
在第一句中,我在want和share之间漏掉了to。在第二句中,Its应该是It’s或It is。
还有一件事。
我刚意识到,在写了Oh, in fact之后……我又引入了一些额外的错误!
好吧,我就停在这里,否则我可能会无限循环,生成带有错误的新文本来解释我在前一版本中犯的错误!
好吧,让我们看看其他例子。
She ran in the bedroom from the living room.
很明显,作者在这个上下文中指的是into。 (如果“from the living room”被遗漏,可能就不那么明确了。)
以下是来自[1]的例子。
Is that the best song you ever heard of?
of是不需要的。
我之前提到过,我经常使用by而应使用with。这里有一个例子。
For linear classification problems one can use linear neurons.
For classification problems that are not linearly separable,
replace linear by sigmoidal.
在上述例子中,linear by sigmoidal应为linear with sigmoidal。
解决方案
如果我们能够访问像 ChatGPT 这样的巨大语言模型[3]并能够按需或批量运行,我们可能就不需要像下面这样一点一点地进行。
我们假设读者没有这样的访问权限。此外,我们假设读者对了解这些用例相关的方法感兴趣,并可能想从头开始实现它们。
正如感兴趣的读者稍后将看到的,这篇帖子中讨论的方法将非常容易从头开始实现。
使用预先构建的大模型不会提供任何这样的见解。也就是说,如果可以获得大型语言模型,尝试一下是个好主意。体验它的行为。观察它能够解决哪些问题。在本文的背景下,评估其解决方案在什么方面优于我们的?
好的,回到我们具体建议的讨论。首先,我们将讨论用于训练的数据集。一个明智选择的数据集足以满足我们所有的用例。
接下来,我们将讨论一些来自统计自然语言处理的“基础构建块”方法。我们将从几个初步用例开始开发这些方法。然后我们将讨论这些相同的方法如何适用于我们在本文早些时候描述的许多其他用例,尽管需要不同的预处理或提取的特征。
也就是说,本文早些时候详细描述的一些用例,我们将不会尝试讨论解决方案。这是因为这些用例似乎需要考虑上下文的更高级方法,即复杂的语言模型。我们将在未来的文章中讨论这些。
训练数据
我们在本文中讨论的所有用例所需的模型原则上可以从一个数据集中学习。一个质量合理的文本文档语料库。
比如维基百科。事实上,人们可以将维基百科的整个文本下载到自己的计算机上。参见[4]。
对于一些问题,即使手动复制几页维基百科或从一些合理质量的网页中复制内容,也足以进行快速的初步训练和评估。
简单计数方法
考虑尝试检测过长的句子或段落。
我们将以如下方式训练这些检测方法。首先,我们将文本中的每个文档标记化为段落,然后将每个段落标记化为句子,最后将每个句子标记化为词语。
可以使用 NLTK 将段落标记化为句子。参见[5]。
将文档分解为段落,再将段落分解为句子本身也是相当有趣的自然语言处理问题。我们将在另一篇文章中涵盖这些方法。这里我们的重点在其他地方。
现在我们可以分别构建句子和段落中的单词数模型。利用这些模型,我们可以标记异常长的句子或异常长的段落。
长度分布的参数模型
我们在这个主题上想要补充的主要内容是,正态分布并不是建模句子或段落长度的一个好选择。
正态分布是对称的,尾部延伸至负无穷大和正无穷大。相比之下,句子长度更可能按以下方式分布。单词句子虽然存在,但相对较少。随着长度的增加,这种长度的句子会变得更常见。随着句子长度的进一步增加,这种长度的句子会变得不那么常见,呈指数级减少。
读者可以通过快速上下滚动这篇文章并目测各种句子的长度来测试这一推理。读者会发现一些两词句子。大多数句子包含五到十个词。读者不会找到包含三十个词的句子。这些句子太长了。
好的,我们回到我们想要表征的分布。它应该是这样的。
泊松分布比正态分布更适合这种形状。见[6]。
泊松分布有一个参数,即一个可调节的旋钮,可以进一步微调形状。这个参数可以从数据中进行调节。
另见[7],这是一篇专注于文本中句子长度分布建模的论文。正如文中讨论的那样,对数正态分布也值得考虑。
长度分布的非参数模型
假设我们的语料库包含大量句子。我们可以避免对分布形式做任何假设。它应该是泊松分布?还是帕累托分布?还是对数正态分布?或者其他什么?
从数据中经验性地估计。这就是说,基本上只是对各种句子长度创建一个直方图。它会像这样。
Num Words 1 2 3 … 8 … 30
Number of Sentences 2 4 10 … 500 … 0
这表明我们语料库中有 10 个三词句和 500 个八词句。
直方图可能需要一些平滑处理,包括插值和外推。
为了说明插值的必要性,请考虑以下场景。在我们的语料库中,假设有一个 50 词的句子出现,但没有 45 词的句子。(不用在意 40 词的句子怎么来的。)我们不想说 45 词的句子出现的概率为零。虽然很小,但不是零。
好的,我们回到直方图的讨论。
一个经过适当平滑的直方图可以用来为新句子打分,以评估其长度的异常程度。一个异常长的句子应该得分很低。
平滑的直方图包含了这种评分所需的所有信息。我们可以称之为“P值或百分位评分”。例如,如果我们语料库中的 99%的句子不超过二十个词,那么一个 21 词的句子的P值不超过 0.01,即 1%。这可以表示为百分位单位的评分。1将意味着非常低的评分。
为了保持以上段落的描述足够简单,我们忽略了如右尾P值与双尾P值的细节。在实际操作中,这并不太重要,除非评分阈值基于P值的截止点。实际操作中,通常并不是这样。相反,它们是基于我们认为截止点应该在何处来校准的。也就是说,我们询问人们认为哪些句子太长,并从这些反馈中得出评分截止点。
涉及特定令牌分布的方法
考虑一下我们在文章中描述的涉及连字符的问题。即是否应该在某些相邻的词之间加连字符?如果不加连字符,是否应该有空格,还是将词融合在一起?
让我们从重复之前看到的例子开始,以更好地理解问题的本质。
He’s an all star runner ⇒
He’s an all-star runner
Chocolate covered truffles ⇒
Chocolate-covered truffles
My co-worker has a four-year old child. ⇒
My co-worker has a four-year-old child
We sat on the plane for deicing for an hour. ⇒
We sat on the plane for de-icing for an hour.
This car is a finely-tuned machine. ⇒
This car is a finely tuned machine.
She attends Ohio-State University. ⇒
She attends Ohio State University.
Heart-broken ⇒ Heartbroken.
作为第一次尝试,即使在没有建模上下文的情况下,我们也能够在检测可疑连字符方面做到相当不错,并提供合理的替代方案。
显然,我们不需要完美的精度或完美的召回率。只需足够让用户找到价值。我们可以始终进行迭代和改进。
我们想指出,我们应该更加关注检测的精度,而不是提出的替代方案的质量。这是因为对于相邻的词,只有三种可能性——加连字符、使用空格或将词粘合在一起。所以如果检测到的连字符确实很差,即使展示其他两种方案也能为用户提供价值。
因此,我们将按以下方式建模该问题。首先,让我们考虑一个发现有连字符的词,例如heart-broken。当我们在训练过程中遇到这个词时,我们将进行如下操作。
我们将创建一个去掉连字符的新词。在我们的例子中,它将是heartbroken。每次在文本中遇到heart-broken时,我们会将heartbroken → heart-broken的实例添加到一个映射中。在 Python 伪代码中,它会像这样:
style_map[‘heartbroken’][‘heart-broken’] += 1
当我们在语料库中遇到heartbroken(而不是heart-broken)时,我们会进行如下操作:
style_map[‘heartbroken’][‘heartbroken’] += 1
所以一旦训练完成,style_map[‘heartbroken’]将会有两个版本heart-broken和heartbroken的分布。
因此,如果 P(heartbroken|heartbroken) 远高于 P(heart-broken|heartbroken),我们会倾向于将heart-broken标记为可疑,并建议使用heartbroken作为改进建议。
如果在评分过程中,单词表示为heart-broken,那么我们会首先去掉连字符,就像我们在训练过程中所做的一样。如果结果键与最可能的重写不匹配,我们会将该实例标记为可疑。在我们的例子中,这将发生,因为heartbroken的最可能重写是heartbroken本身。但是文本中却是heart-broken。
现在让我们考虑一下如何在训练语料库中建模相邻的词。不加连字符。例如boat house。它的表达更好是boat house、boathouse还是boat-house?
为了覆盖这种情况,我们只需要一些额外的逻辑,如下所示。
以两个相邻的词boat house为例,我们将衍生出一个新词boathouse,将这两个词融合在一起,并将boat house作为键添加到boathouse中。如下所示。
style_map[‘boathouse’][‘boat house’] += 1
我们会期望在语料库中boathouse的出现频率远高于boat house。也就是说,P(boathouse|boathouse)将远大于P(boat house|boathouse)。
连字符建模中的概化
所谓概化,是指学习涉及连字符的规则,这些规则超出了我们在训练数据中遇到的具体实例。
继续阅读,以查看在这种情况下的具体概化类型。
涉及特定前缀的概化
从训练语料库中,我们可能会观察到单词very后面从未跟随连字符。因此
A very-happy dog
是不正确的。应该是very happy。
这个例子,或者更一般地说,这个规则,来自[2]。
我们能否从数据本身学习到这个规则?
是的。这里是方法。
我们将使用第二个映射,称为hyphenation_prefix_map。这个映射的键将是前缀。每次我们在训练语料库中看到这个前缀后面紧跟连字符时,我们将把值“1”的计数加一。每次我们在训练语料库中看到这个前缀后面没有连字符时,我们将把值“0”的计数加一。
上述段落中的逻辑伪代码如下:
if the current word w is immediately followed by a hyphen:
hyphenation_prefix_map[w][1] += 1
else:
hyphenation_prefix_map[w][0] += 1
现在让我们展示当我们在评分文本中看到very-happy时的处理方法。假设我们从语料库中学到
P(下一个字符是-|当前单词是very)
是零,或者几乎为零。我们会将very-happy标记为可疑,并建议去掉连字符。
还要注意,这个解决方案还涵盖了在[2]中进一步描述的规则。
Hyphenate all words beginning with the prefixes self-, ex-, and all-
以下是示例,也来自[2]。
She is now self-employed.
My ex-classmate took my notes.
We are going to an all-inclusive resort.
实际上,我们的数据驱动学习将比 100%遵循这个规则做得更好。让我们详细说明一下。
有些以self开头的单词没有连字符。例如,selfish。
所以我们的模型将学习到通用规则
if word starts with self
self is followed by a hyphen
以及像selfish这样的例外,只要它们出现在训练语料库中。
需要稍微调整一下,以确保规则“selfish stays selfish”能够触发,而不是通用规则。这很简单,但我们将把它留给读者作为练习。
涉及数字的概化
请考虑来自[2]的这些示例。
My co-worker has a four-year-old child.
Their child is four years old.
第一个连字符用法是正确的。four-year old的连字符用法则不正确。第二句话中缺少连字符的用法也是正确的。
假设术语four-year-old在训练语料库中出现频繁。扩展我们之前描述的方法以处理三个相邻的词素,我们可以检测到four-year old实际上应该是four-year-old。
那么,如果我们要评分的文本包含术语hundred year old呢?假设这个确切的术语在训练语料库中没有出现。我们希望能够将其标记为可疑,并建议作者考虑将其重新表述为hundred-year-old。
这是另一个概化的例子。
显然,我们希望能够从实际出现的实例中学习模式 <number>-year-old。
如果我们在语料库中看到实例 1-year-old、2-year-old、one-year-old、two-year-old 等,从这些实例中我们可以合理推测模式为 <number>-year-old。
我们将高层次地概述一种解决此问题的方法。我们将在下面进行说明。
考虑语料库中的实例 2-year-old。如前所述,我们将更新 stylemap 对于键 2yearold。此外,我们还将进行以下操作。我们将使用命名实体识别器将 2 识别为数字,可能是一个结合了基于词典和正则表达式的方法的简单工具。
请参见 [8],获取关于 NLP 中命名实体识别的详细帖子,涵盖了这一层次的场景以及更复杂的场景。
我们可以表达结果为键 <num>-year-old。我们像以前一样更新这个键。也就是说,
style_map[<num>-year-old][<num>-year-old] += 1
现在让我们看看当我们在文本中看到 100 year-old 时需要做什么。首先,我们去掉连字符,查找 100yearold 在 style_map 中。假设它不存在。然后我们将 100 识别为 <num> 并尝试在 style_map 中查找 <num>yearold。它作为键存在。接下来,我们找到具有最高概率且差距足够大的样式。在我们的例子中,它将是 <num>-year-old。接着,我们用 100 替换 <num>。最后,我们提供结果 100-year-old 作为建议的重表达。
接下来,考虑以下示例,亦来自 [2]。
There are fifty-seven kids in that grade.
用 fifty seven 替换 fifty-seven 将是不正确的。
我们可以从数据中自动学习这一模式。为此,我们希望在命名实体中引入进一步的区别:拼写出的数字 与 书写出的数字。Five 是 拼写出的数字。5 是 书写出的数字。凭借这一区别,我们可以学习到规则是
if two adjacent tokens are spelled out numbers with a space in between them
insert a hyphen between the two
这种方法可以进一步完善,以涵盖如下所示的示例,也来自 [2]。
They need two-thirds of the vote to win.
two thirds 将是不正确的。
单词大小写
这种方法也适用于某些单词大小写的场景。例如 fyi 更好地表示为 FYI。我们只需以不同的方式预处理标记。如下所示。
FYI ⇒ fyi ⇒ style_map[fyi][FYI] += 1
也就是说,当我们在语料库中遇到 FYI 时,首先我们将其全部小写,然后再添加一个 FYI 的实例,以便与 fyi 关联。
一旦训练完成,假设语料库足够干净且丰富,P(FYI|fyi) 应该远大于 P(fyi|fyi)。因此,如果我们在被评分的文本中遇到 fyi,我们将建议将其替换为 FYI。
其中的撇号
假设语料库中特定句子的第一个词是 Its。它实际上应该是 It’s。
我们如何建模以检测此类错误并推荐特定的修正?
相同的方法在这里也适用,只是预处理稍有不同。
考虑语料库中It’s的出现。我们将进行以下操作。
It’s ⇒ its ⇒ style_map[its][It’s] += 1
也就是说,我们去掉撇号得到Its。然后,我们再添加一个It’s与Its相关联。
请注意,在这种情况下,我们没有将It’s小写,只是去掉了撇号。这是因为我们希望保留It’s 中的i为大写。
一旦训练完成,假设语料库足够干净且丰富,P(It’s|Its)应该比P(Its|Its)大得多。因此,如果我们遇到its作为句子中的第一个词被评分,我们将首先将其转换为Its,然后建议将这个Its替换为It’s。
实际上,如果我们将Its视为句子的第一个词,我们应该建议将其替换为It’s或It is。我们可以扩展我们的模型以直接学习提供第二个建议。我们将把这个问题留给读者作为练习。
缺失的逗号
考虑我们之前看到的示例。
Regardless there is no way …
Clearly the writer meant …
在每种情况下,句子的第一个词后面应该有一个逗号。
下面,我们将深入探讨如何在这种场景中检测缺失的逗号。我们将仅关注这些场景,特别是那些在句子的第一个词或两个(或三个)词后面应该跟随逗号的情况。
在更微妙的情况下,例如在句子中间的位置,检测逗号会更复杂。我们将在未来的帖子中解决这个问题。
我们将使用之前在其他用例中使用的相同方法。不同的是,我们将仅应用于句子开头的词。
对于训练语料库中每个以句子开头的词,我们将跟踪其后是否跟随逗号的频率。在训练后,我们可以以我们之前描述的方式检测缺失的逗号。
这与关联规则的关系
我们在这篇文章中讨论的主要方法可以看作是挖掘关联规则的一个实例。
从这个角度看,我们使用的各种映射数据结构编码了特定形式的关联规则。
IF X THEN Y
我们使用的概率推断,用于检测特定规则是否应该触发,称为在挖掘关联规则中的confidence。它的形式是P(Y|X)。
在关联规则挖掘中,另一个常用的替代方法叫做lift。有关关联规则和 lift 的更多信息,请参见[9]。
总结
在这篇文章中,我们讨论了在文本写作过程中检测和纠正问题的话题。这个用例具有巨大价值,从改善正在写作的电子邮件,到帮助作者在撰写较长文章时提高写作质量。
这就是 Grammarly 如此受欢迎的原因。
在这篇文章中,我们列举了以下类型的常见错误及其示例。
-
缺少逗号。
-
缺少冠词。
-
在It’s中遗漏撇号。
-
使用单数形式而不是复数形式,或反之亦然。
-
使用连字符时不该用,或反之亦然。
-
大小写问题。应大写的字母未大写。应全大写的单词未全大写。例如,fyi
-
使用错误的介词,或在不需要时使用介词。
在每种情况下,我们都通过实际例子描述了问题。通常会出现细微的差别。
接下来,我们对这些问题进行了第二次审视,讨论了统计自然语言处理的方法,这些方法有助于检测和解决这些问题。我们讨论了训练数据、特征工程以及适用于这些用例的一些特定统计模型。
参考文献
-
自然语言处理中的命名实体识别。真实世界的应用案例、模型、方法……
时间序列模型中的文本数据预处理
原文:
towardsdatascience.com/text-data-pre-processing-for-time-series-models-162c0d01f5c5
你是否曾考虑过如何将文本数据中的情感用作时间序列模型中的回归器?
·发表于 Towards Data Science ·6 分钟阅读·2023 年 2 月 9 日
--
照片由 Kaleidico 在 Unsplash 提供
介绍
文本数据提供了可以量化、聚合并用作时间序列模型中的变量的定性信息。从 NLP 的早期开始,简单的文本数据表示方法,如独热编码的类别变量和词 n-gram,已经被使用。随着时间的推移,包括词袋模型在内的更复杂的方法,开始用于表示文本数据以供机器学习算法使用。基于 Harris [1] 和 Firth [2] 提出的分布假设,现代模型如 Word-to-Vec [3] 和 [4]、GloVe [5] 和 ELMo [6] 在其神经网络架构中使用词的向量表示。由于计算机将文本处理为向量,因此它可以作为时间序列计量经济模型中的变量使用。
通过这种方式,我们可以利用文本中的定性信息,扩展定量时间序列模型的可能性。
在本文中,你将深入了解:
-
如何将文本中的定性信息用于定量建模
-
如何清洗和表示时间序列模型中的文本数据
-
如何高效处理100 万行文本数据
-
Python 中的端到端编码示例。
在我们最近的会议论文中,我们制定了一个文本数据预处理的结构性计划,可能用于以下领域:(1)利用社交网络的情感预测汇率,(2)使用公开新闻数据预测农业价格,(3)在各个领域进行需求预测。
1. 文本数据表示的结构计划
让我们从计划开始。开始时,我们有定性原始文本数据,随着时间的推移进行收集。最后,我们得到的是具有时间变化的数值向量(=定量数据)。这个图表更清楚地说明了我们将如何进行:
图 1. 文本数据表示的结构计划。来源:Poměnková 等人,提交至MAREW 2023。
2. Python 中的经验示例
让我们通过新闻类别数据集展示编码,该数据集由 Rishabh Misra 编制[8],[9],并根据署名 4.0 国际许可证发布。数据包含 2012 年至 2022 年间在 huffpost.com 上发布的新闻标题,已扩展到 100 万行的数据集。
主要目标是从新闻标题中构建按月频率的时间序列,反映公众情绪。
数据集包含 100 万个标题。由于其规模,我使用了Polars库,这使得数据框操作更加高效。与主流的 Pandas 相比,它处理大型数据文件的效率更高。此外,代码在 Google Colab 上运行,使用了 GPU 硬件加速器。
Python 代码在这里,数据如下:
图 2. 新闻类别数据集
2.1. 文本数据预处理
文本数据预处理的目的是去除所有可能偏倚分析或导致结果不准确解释的冗余信息。我们将移除标点符号、数字、多余的空格、英语停用词(最常见的、信息量低或为零的词),并将文本转为小写。
可能最简单且最有效的文本数据清理方法是使用cleantext库。
首先,定义一个清理函数以执行清理操作:
def preprocess(text):
output = clean(str(text), punct=True,
extra_spaces=True,
stopwords=True,
lowercase=True,
numbers = True)
return output
接下来,我们使用 Polars 清理 1 百万数据集:
data_clean = data.with_columns([
pl.col("headline").apply(preprocess)
])
清理后的数据集包含最大信息量的文本,便于进一步处理。任何不必要的字符串和数字都会降低最终经验建模的准确性。
2.2. 文本数据表示
数据表示涉及在计算机中表示数据的方法。由于计算机处理的是数字,我们选择了一个适当的模型来向量化文本数据集。
在我们的项目中,我们正在构建情感的时间序列。对于这种用例,预训练的情感分类器**VADER (情感词典和情感推理器) **是一个不错的选择。阅读我的上一篇文章以了解更多关于该分类器的信息以及其他一些替代方案。
使用vaderSentiment库进行分类的代码如下。首先,创建分类函数:
from vaderSentiment.vaderSentiment import SentimentIntensityAnalyzer
# calculate the compound score
def sentiment_vader(sentence):
# create a SentimentIntensityAnalyzer object
sid_obj = SentimentIntensityAnalyzer()
sentiment_dict = sid_obj.polarity_scores(sentence)
# create overall (compound) indicator
compound = sentiment_dict['compound']
return compound
接下来,应用时间序列数据集的函数:
# apply the function with Polars
sentiment = data_clean.with_columns([
pl.col("headline").apply(sentiment_vader)
])
结果如下:
图 3. 情感评估
标题 列包含情感,范围为[-1:1],反映每行标题中的主要情感内容。
2.3. 时间序列表示
时间序列文本数据表示的下一步是扩展数据矩阵的时间维度。可以通过 (a) 沿时间轴聚合数据和 (b) 选择实现时间序列文本数据表示的方法来实现。在我们的数据中,我们将做前者,并按月频率聚合每行的情感。
这段代码对情感进行平均聚合,并准备每月的时间序列:
# aggregate over months
timeseries = (sentiment.lazy()
.groupby("date_monthly")
.agg(
[
pl.avg("headline")
]
).sort("date_monthly")
).collect()
2.4. 定量建模
最后的步骤是使用时间序列进行建模。以我们的最近会议论文为例,我们类似地从前五名经济学期刊发表的研究文章标题中提取了情感。然后,我们使用 5 年窗口的滚动时间变动相关性,并观察情感与 GDP 及其他全球经济指标的关系(见图 2)。
我们假设情感与宏观经济环境在经济急剧衰退和通货膨胀冲击期间相关。结果支持这种考虑,除了一个特定的期刊,关于 1970 年代的石油冲击,这导致了急剧的衰退伴随着巨大的通货膨胀峰值。
图 4. 情感与 GDP 的滚动相关性。来源:Poměnková et al.,提交至MAREW 2023。
结论
在这篇文章中,我们从 100 万行文本数据中构建了每月的情感时间序列。关键点包括:
-
定性信息可以扩展定量时间序列模型的能力。
-
Polars 库使得即使在 Python 语言中,也能实现大规模文本数据预处理的可行性。
-
诸如 Google Colab 的云服务使得处理大规模文本数据集的速度更快。
本教程的完整代码在我的 GitHub 上。推荐阅读 Python 中最受欢迎的预训练情感分类器。
你喜欢这篇文章吗?你可以邀请我 喝咖啡 来支持我的写作。你也可以订阅我的 邮件列表 以便接收我的新文章通知。谢谢!
参考文献
[1] Z. Harris. 1954. 分布结构。Word,第 10 卷,第 23 期,第 146–162 页。
[2] J. R. Firth. 1957. 语言学理论的概述 1930–1955. 收录于《语言学分析研究》,第 1–32 页. 牛津:语言学会。重印于 F.R. Palmer (编),《J.R. Firth 1952–1959 选集》,伦敦:Longman 1968。
[3] Mikolov, T., Chen, K., Corrado, G. S., Dean, J. 2013b. 向量空间中词表示的高效估计。计算与语言:国际学习表示会议。
[4] T. Mikolov, I. Sutskever, K. Chen, G. S. Corrado 和 J. Dean. 2013. 词汇和短语的分布式表示及其组合性。神经信息处理系统进展,第 26 卷 (NIPS 2013)。
[5] M. E. Peters, M. Neumann, M. Iyyer, M. Gardner, C. Clark, K. Lee 和 L. Zettlemoyer, L. 2018. 2018 年北美计算语言学协会会议:人类语言技术会议论文集,第 1 卷。
[6] J. Pennington, R. Socher 和 C. D. Manning. 2014. GloVe:全局词向量表示。发表于 2014 年自然语言处理实证方法会议(EMNLP)论文集。
[7] Poměnková, J., Koráb, P., Štrba, D. 时间序列建模的文本数据预处理。提交至 MAREW 2023。
[8] Misra, Rishabh. “新闻类别数据集。” arXiv 预印本 arXiv:2209.11429 (2022)。
[9] Misra, Rishabh 和 Jigyasa Grover. “为 ML 雕刻数据:机器学习的第一幕。” ISBN 9798585463570 (2021)。
自动化化学实体识别:创建你的 ChemNER 模型
·发表于Towards Data Science ·15 分钟阅读·2023 年 11 月 16 日
--
照片由Aakash Dhage拍摄,来源于Unsplash
我一直对化学有浓厚的兴趣,它在塑造我的学术和职业旅程中发挥了重要作用。作为一个具有化学背景的数据专业人员,我发现有许多方法可以将我的科学和研究技能如创造力、好奇心、耐心、敏锐观察和分析应用于数据项目。在这篇文章中,我将引导你开发一个我称之为 ChemNER 的简单命名实体识别(NER)模型。这个模型可以识别文本中的化学化合物,并将它们分类为烷烃、烯烃、炔烃、醇、醛、酮或羧酸等类别。
TL;DR
如果你只是想玩玩 ChemNER 模型和/或使用我制作的 Streamlit 应用,你可以通过以下链接访问它们:
HuggingFace 链接: huggingface.co/victormurcia/en_chemner
Streamlit 应用: ChemNER 链接
介绍
NER 方法通常可以分为以下三类:
-
基于词典:定义类别和术语的字典
-
基于规则:定义每个类别对应的术语规则
-
基于机器学习(ML):让模型从训练语料库中学习命名规则
这些方法各有其优缺点,并且一如既往,复杂而精细的模型并不总是最佳方案。
在这种情况下,基于词汇表的方法在范围上会有限,因为对于我们感兴趣的每一类化合物,我们都需要手动定义该类别中的所有化合物。换句话说,为了使这种方法全面,你需要手动输入每个化合物类别的所有化合物。
机器学习方法可能是最强大的选择,然而,注释数据集可能非常繁琐(剧透:我会训练一个模型,但我想展示整个过程以供学习)。那么,我们不妨从一些预定义的命名规则开始?
化学命名法有一套完善且明确的规则,使你能够轻松确定分子中存在的功能团。这些规则由国际纯粹与应用化学联合会(IUPAC)制定,可以通过IUPAC 蓝皮书、各种网站或任何有机化学教科书轻松获取。例如,烃是仅由碳和氢原子组成的化合物。烃主要有三类,分别是烷烃、烯烃和炔烃,可以根据其化学结构中是否包含单键、双键或三键来识别。下面我展示了三个化学化合物(乙烷、乙烯和乙炔)的例子。
乙烷、乙烯和乙炔。图片作者提供。
对我们来说,重要的是名字的结尾(即后缀),因为这将使我们能够区分化学化合物。例如,烷烃由后缀* -ane* 标识,烯烃由后缀* -ene* 标识,炔烃由后缀* -yne* 标识。每类化学化合物如醇、酮、醛、羧酸等都有独特的命名方案,这些方案将作为该项目的基础。
建立规则
现在我们有了一些背景知识来理解发生了什么,我将展示如何使用 Spacy 在 Python 中实现基于规则的方法。我将从处理烃开始,稍后会添加其他类别。为此,我们首先将使用 Spacy 加载一个空白的英语模型,并将‘实体规则器’组件添加到我们的管道中:
# Load a blank English model
nlp = spacy.blank("en")
#Create the EntityRuler
ruler = nlp.add_pipe("entity_ruler")
接下来,我们将建立定义每一类的规则/模式,并将其添加到规则组件中:
# Define patterns
patterns = [
{"label": "ALKANE", "pattern": [{"TEXT": {"REGEX": ".*ane$"}}]},
{"label": "ALKENE", "pattern": [{"TEXT": {"REGEX": ".*ene$"}}]},
{"label": "ALKYNE", "pattern": [{"TEXT": {"REGEX": ".*yne$"}}]}
]
ruler.add_patterns(patterns)
就这样!现在让我们创建一些文本以供模型使用,看看效果如何!
text = "Ethane, propene, and butyne are all examples of hydrocarbons."
doc = nlp(text)
#extract entities
for ent in doc.ents:
print (ent.text, ent.start_char, ent.end_char, ent.label_)
结果如下:
Ethane 0 6 ALKANE
propene 9 16 ALKENE
butyne 22 28 ALKYNE
非常好!然而,你可能已经注意到这个初始方法有两个直接的局限性:
-
当前的正则表达式无法检测化合物的复数形式。
-
仅基于后缀的分类会导致很多错误标记的实体。
尽管化学化合物通常被视为不可数名词(想想像 air 或 music 这样的词),但在某些情况下,复数形式仍然可以使用。例如,如果你处理的是一组乙烷分子,有人可能会将其称为一组乙烷。因此,第一个问题可以通过将我们的正则表达式修改为以下形式来轻松解决:
# Define patterns
patterns = [
{"label": "ALKANE", "pattern": [{"TEXT": {"REGEX": ".*anes?$"}}]},
{"label": "ALKENE", "pattern": [{"TEXT": {"REGEX": ".*enes?$"}}]},
{"label": "ALKYNE", "pattern": [{"TEXT": {"REGEX": ".*ynes?$"}}]},
]
现在,实体规则器将识别单数和复数形式。然而,第二点仍然存在。例如,如果文本中出现像 arcane、humane、thane、lane 和 mundane 这样的词,它们会被错误标记为烷烃。
尽管还有其他规则可以实施以增强这种方法,但它们会需要相当多的额外工作。因此,我考虑了三种方法来处理我们的限制:
-
构建一个语料库,以训练用于此应用程序的基于机器学习的命名实体识别模型
-
使用命名实体链接(NEL)来帮助纠正模型输出中出现的标注错误
-
对像 SciBERT 或 PubMedBERT 这样的变换器模型在自定义数据集上进行微调
对于这篇文章,我将仅涵盖前两种方法。然而,如果有兴趣,我将在未来的文章中展示如何完成微调过程。
创建数据集
创建语料库有多种不同的方法。生成这个语料库的快速而简单的方法是让 chatGPT 创建包含我想从文本中提取的各种类别的化合物的句子集。之所以这样做效果很好,是因为这种方法允许我策划和调整我的数据集,这使得后续的标注过程变得更容易。我的提示是:
Give me a set of 50 unique sentences each dealing with unique alkanes
然后,我对我感兴趣的其他类别(即,烯烃、炔烃、醇、酮、醛和羧酸)重复了那个提示。由于我有 7 个类别,我最终得到了 350 个句子组成的语料库。理想情况下,这个语料库会更大,但这是一个很好的开始,因为我主要想说明这是一个概念验证。再者,通常可以根据需要添加更多数据来提高性能。我将我的句子保存到一个名为 chem_text.txt 的文档中。
为 ChemNER 制作的语料库的截图。图片由作者提供
作为最后一步,我会使用句子分词器将文档中的每个句子分开。
doc = nlp(chem_text)
corpus = []
for sent in doc.sents:
corpus.append(sent.text.strip())
现在我已经制作了这个语料库,我们需要开始对其进行标注。有几种方法可以做到这一点。例如,我们可以使用像Prodigy这样的注释工具(它非常棒,如果你做任何类型的自然语言处理,应该使用它),或者我们可以使用之前的基于规则的方法来帮助我们进行初步标注。现在,由于我不是在标注一个庞大的数据集,我会使用模型方法。
DATA = []
#iterate over the corpus again
for sentence in corpus:
doc = nlp(sentence)
#remember, entities needs to be a dictionary in index 1 of the list, so it needs to be an empty list
entities = []
#extract entities
for ent in doc.ents:
#appending to entities in the correct format
entities.append([ent.start_char, ent.end_char, ent.label_])
DATA.append([sentence, {"entities": entities}])
为了包含我感兴趣的所有类别,规则需要更新为以下内容:
# Define patterns
patterns = [
{"label": "ALKANE", "pattern": [{"TEXT": {"REGEX": ".*anes?$"}}]},
{"label": "ALKENE", "pattern": [{"TEXT": {"REGEX": ".*enes?$"}}]},
{"label": "ALKYNE", "pattern": [{"TEXT": {"REGEX": ".*ynes?$"}}]},
{"label": "ALCOHOL", "pattern": [{"TEXT": {"REGEX": ".*ols?$"}}]},
{"label": "ALDEHYDE", "pattern": [{"TEXT": {"REGEX": ".*(al|als|aldehyde|aldehydes)$"}}]},
{"label": "KETONE", "pattern": [{"TEXT": {"REGEX": ".*ones?$"}}]},
{"label": "C_ACID", "pattern": [{"TEXT": {"REGEX": r"\b\w+ic\b"}}, {"TEXT": {"IN": ["acid", "acids"]}}]}
]
运行基于规则的方法的结果使我们可以快速标注我们的数据集,如下所示。
ChemNER 的标注语料库。图片由作者提供。
我们已经接近将语料库分为训练集和测试集,但在继续之前,我们需要验证标注的质量。检查数据集时,我注意到出现了之前提到的错误标注问题。数据集中出现了“essential”、“crystals”、“potential”、“materials”等词语,这些词被标注为醛类,这突显了基于规则的方法的局限性。我使用下面的方法手动移除了这些标签,并重新处理了语料库上的标注:
# List of words to be ignored
ignore_set = {"essential", "crystals", "potential","materials","bioorthogonal","terminal","chemicals",
"spiral","natural","positional","structural","special","yne","chemical","positional",
"terminal","hormone","functional","animal","agricultural","typical","floral","pharmaceuticals",
"medical","central","recreational"} # Convert ignore list to set
DATA = []
# Iterate over the corpus
for sentence in corpus:
doc = nlp(sentence)
entities = []
# Extract entities
for ent in doc.ents:
# Check if entity is not in the ignore set
if ent.text.lower() not in ignore_set:
# Appending to entities in the correct format
entities.append([ent.start_char, ent.end_char, ent.label_])
DATA.append([sentence, {"entities": entities}])
现在我们已经准备好创建训练集和测试集。这可以通过 scikit-learn 中的 train_test_split 函数轻松完成。我使用了标准的 80:20 训练:测试划分。
# Split the data
train_data, valid_data = train_test_split(DATA, test_size=0.2, random_state=42)
训练模型
我们的训练数据已经准备好,我们可以开始训练模型。为了训练模型,我使用了默认的 Spacy NER 训练参数,如 Adam 优化器和 0.001 的学习率。训练在 Google Colab 的 CPU 上花费了一个多小时,如果使用 GPU,则时间会大大缩短。训练结果如下所示:
ChemNER 模型的训练结果。图片由作者提供。
上述图表显示了该模型在训练过程中 F1 得分、准确率、召回率和整体得分的趋势都在上升,这很好。与 NER 组件相关的 NER 损失总体上趋向于最小值。该模型的最终性能得分为 0.97,看起来很有前景。
然而,Tok2Vec 损失在大约第 300 个 Epoch 时明显上升,这可能是由于学习率过高、梯度消失/爆炸导致数值不稳定或过拟合等问题。Tok2Vec 损失表示模型中负责将令牌转换为向量的 token-to-vector 部分的有效性。如果我们选择,可以有多种方式来处理这个问题,但现在,我会继续进行。
测试模型
让我们从简单的测试开始。我将输入几句话,看看它的分类效果如何。结果如下所示:
ChemNER 模型的初步测试。图片由作者提供。
很棒!它提取了所有相关实体,并且全部标注正确!这就是机器学习方法的酷炫之处。与其我们显式编写规则,不如算法在训练过程中自我学习。虽然这很酷,但现在我们来对模型施加更多压力。
查询维基百科(压力测试)
我想对我的模型进行更多的压力测试,因此我认为快速且简单的方法是将整个维基百科文章输入模型,看看它的表现。我将编写一个简单的程序,通过 Python 的 wikipedia-api 包来实现:
import wikipediaapi
# Define your user agent
user_agent = "MyApp/1.0 (your@email)"
# Initialize Wikipedia API and spaCy
wiki_wiki = wikipediaapi.Wikipedia(user_agent,'en')
# Function to get Wikipedia article
def get_wikipedia_article(page_title):
page = wiki_wiki.page(page_title)
return page.text if page.exists() else None
# Function to perform NER on text
def perform_ner(text):
doc = nlp(text)
return [(ent.text, ent.label_) for ent in doc.ents]
接下来,我将查找有关苯的维基百科文章:
# Query Wikipedia for an article
article_title = "Benzene" # Replace with your desired article title
article_content = get_wikipedia_article(article_title)
这样产生的结果是:
苯维基百科文章查询的截图。
很棒!现在我们已经验证了查询工作正常,让我们运行 ChemNER 模型。ChemNER 模型从苯的文章中提取了总共 444 个实体。这些实体的提取时间不到一秒。我将结果放入数据框中,并在下面的计数图中可视化标签计数:
ChemNER 对苯维基百科文章的结果。图片由作者提供。
该文章中最常见的类别是烯烃,这很有意义,因为苯对应的正是这一化合物类别。我觉得有点意外的是,这篇文章中包含了每个类别的实体。
这很有趣,不过,通过快速检查数据框中提取的实体的前几行,我们可以看到模型存在一些问题。‘chemical’ 和 ‘hexagonal’ 被标注为醛,而‘one’ 被标注为酮。这些显然不是化学化合物,不应被分类为此。我手动识别了每个实体是否正确,并确定提取的准确率为 70.3%。尽管所有提取的实体根据模型学到的规则都被标注为‘正确’,但模型尚未真正理解词汇的上下文。
ChemNER 对苯的文章中正确和错误标注的实体比较。图片由作者提供。
不过我注意到一个有趣的地方,即正确标注的实体全都是化学化合物。换句话说,如果我们能确定一个实体是否是化学化合物,那么我们可以显著提高该应用程序的标注性能。
目前,我们可以采取几种途径。其中一种途径是回到语料库中生成更多的数据,以便为我们的模型提供学习示例。另一种途径是使用命名实体链接(NEL)来帮助纠正标注。由于后者耗时较少,我决定选择这个选项。
使用 PubChem 进行 NEL
ChemNER 模型在对化学物质进行标记时表现非常出色,只要实体是化学化合物。为了更好地告知模型,我将通过他们的 API 连接到PubChem并进行化学化合物的查询。这里的想法是,对化学化合物的查询会返回信息,而对非化学化合物的查询会返回空结果。我可以利用这些查询结果来提升应用的标记性能。
作为展示示例,我们先查询苯。以下代码可以用来查询 PubChem API。
def get_compound_info(compound_name):
base_url = "https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/name"
response = requests.get(f"{base_url}/{compound_name}/JSON")
if response.status_code == 200:
return response.json()
else:
return None
compound_name = "benzene"
compound_info = get_compound_info(compound_name)
该查询的结果如下所示。
通过 PubMed API 查询苯的结果。图像来源:作者。
从这个查询中我们获得了大量关于苯的信息,后续可以使用。但现在,唯一重要的是查询返回了结果。另一方面,如果我使用相同的方法查询非化学化合物,如‘humans’或‘giraffe’,那么查询结果将是‘None’。
在 PubMed API 上查询非化学化合物。图像来源:作者。
我可以利用这一点来辅助我的应用程序。查询速度相当快,不过,为了加快处理速度,我将从数据框中删除任何重复的实体,以便只查询唯一的术语。此外,PubChem API 似乎假设我们是在查询单一化学化合物,因此像 cinammaldehydes 这样的词会返回空查询。可以通过去掉任何复数形式的终结‘s’来轻松解决此问题。我使用了以下代码在数据框中创建了一个名为‘Chemical Compound’的新列,这样我就可以根据查询结果将每个实体分类为化学化合物或非化学化合物。
这效果相当不错!不过,我在这样做时注意到的一件事是,类标签本身会导致空查询。换句话说,如果我查询 PubChem 的烷烃、烯烃、炔烃等,我会得到一个空查询,因为这些本身不是具体的化合物,而是化合物的类别。在这方面有一些细微的差别。我决定要让这些化合物类别被识别为化学实体,因为类标签可以独立地用于没有具体化合物的句子中(例如,烷烃常见于石化应用)。为了解决这个问题,我简单地添加了一个例程来检查实体列中的条目是否是我们类标签的单数或复数变体,如果实体与标签匹配,则将 Chemical Compound 列中的值设置为 1,否则为 0。
# List of specific chemical compound types
chemical_compounds = ['alkane', 'alkene', 'alkyne', 'ketone', 'aldehyde', 'alcohol', 'carboxylic acid']
# Function to update 'Chemical Compound' column
def update_chemical_compound(row):
entity = row['Entity'].lower()
if any(compound in entity for compound in chemical_compounds + [c + 's' for c in chemical_compounds]):
return 1
return row['Correct']
# Apply the function to each row
df_unique['Chemical Compound'] = df_unique.apply(update_chemical_compound, axis=1)
很棒!现在我可以将这些结果合并到包含所有 444 个结果的原始数据框中。
df_merged = pd.merge(df_ents2, df_unique[['Entity', 'Chemical Compound']], on='Entity', how='left')
使用 PubChem API 检查实体是否为化学化合物后的实体数据框。图片来源:作者。
接下来,我将删除任何与化学化合物不对应的行。
# Dropping rows where 'Chemical Compound' is 0
df_filtered = df_merged[df_merged['Chemical Compound'] != 0]
去除非化学化合物实体后的数据框。图片来源:作者。
现在让我们看看它的表现如何!
ChemNER 在通过 PubChem 执行 NEL 后的结果。图片来源:作者。
非常好!所有提取的实体现在都被正确标注。通过将我们的 NER 模型与 PubChem 的 NEL 结合使用,我们现在不仅能够从文本中提取实体,还能够消歧义结果,从而大大提高我们的标注准确性。
将模型部署到 HuggingFace
作为一个小奖励,我认为将我展示的所有这些例程部署到 HuggingFace 上会很酷,这样我就可以在 Streamlit 应用中展示它。你可以在 HuggingFace 找到这个模型:huggingface.co/victormurcia/en_chemner
。下面展示了推断 API 的结果,看起来相当不错:
ChemNER 在 HuggingFace 推断 API 中的实际应用。图片来源:作者。
如果你使用了它或者有任何建议,请告诉我!我计划未来扩展模型,还有其他功能我想要探索。
使用 Streamlit 应用连接一切
现在模型已经部署,我可以在 Streamlit 应用中使用它。这个应用允许用户链接到维基百科文章或输入原始文本,然后由 ChemNER 模型处理。此过程的输出将是一个可下载的数据框,包含提取和标注的实体、显示每个标签在提供的文本中计数的计数图,以及文本的完全注释版本。你可以在这里找到 Streamlit 应用:chemner-5i7mrvyelw79tzasxwy96x.streamlit.app/
ChemNER Streamlit 应用的截图。图片来源:作者。
举个例子,我将使用应用对以下关于苯的维基百科文章进行查询。结果是文章的注释版本,如下所示,其中每个类别都有独特的颜色编码。
ChemNER 注释的文本。图片来源:作者。
输出也是一个数据框,你可以下载为 .csv 文件,包含实体及其相应的标签,以及一个显示计数的计数图
来自 Streamlit 应用的输出。图片来源:作者。
结论
希望你觉得这篇文章有信息量,并且对你构建自己的 NLP 应用有所帮助。我计划继续在这个模型和应用上做一些工作,因为我认为还有一些有趣的内容我想进一步探索。例如,经过一些测试,我注意到模型提取出的某些实体被 PubChem 方法归类为化学化合物,但实际上它们并不是有机化合物。例如,‘pm’这个词被提取为一个实体,并被标记为醛。PubChem 搜索返回了一个非空的查询,因为‘pm’(更准确地说是 Pm)是元素铽的化学符号。这个模型并不完美,但我希望它能展示出你可以在不需要 LLM 的情况下获得一个相当强大的工具。
一如既往,感谢你的阅读!
文本模式提取:比较 GPT-3 和 人工在环工具
原文:
towardsdatascience.com/text-pattern-extraction-comparing-gpt-3-human-in-the-loop-tool-f2380fd13cf1
比较 LLM 和人工在环工具在文本模式提取中的初步实验和结果
·发布于 数据科学的前沿 ·10 分钟阅读·2023 年 1 月 26 日
--
图片来源于 Aaron Burden 在 Unsplash
在过去几年中,AI 在多个工业应用中引起了广泛关注。医生、分析师和记者等最终用户希望为其特定的使用案例构建 AI 模型。
然而,构建 AI 模型的工作流程需要技术专长,而最终用户可能不一定具备:
-
准备数据,例如提取、清理和转换训练数据。
-
训练 AI 模型,包括微调参数和重新训练模型的层。
动机:使最终用户能够构建 AI 模型。图片来源于作者。
最终用户可能会直接使用现有工具开箱即用。有几种工具适用于最终用户以开箱即用的方式构建 AI 模型。
最近,一类称为人工在环 (HITL) 工具的工具旨在降低最终用户构建 AI 模型的门槛。“人工在环”在模型构建过程中融入了人类知识。它本质上是一个人机协作的框架。在这个框架中,模型构建和用户输入之间有一个持续的反馈过程。
另一方面,AI 领域有一些广泛研究的模型,即大型生成语言模型,如 GPT-3、OPT、BLOOM 等。语言模型在大规模数据集上进行训练,具有惊人的语言理解能力。这些模型的规模达到数亿参数,并且在多个自然语言处理任务(例如提取、分类等)中表现出了少量样本的优异性能(即它们只需少量输入)。
现有工作。图像由作者提供。
我们想了解这些广泛研究的大型语言模型在帮助终端用户构建 AI 模型的背景下,与人机协作工具的表现如何。在我们的实验中,我们模拟了一个终端用户或业务用户(即没有太多技术培训的人)如何使用这些工具来执行文本模式提取任务。 在这篇简短的博客文章中,我想谈谈我们在 IBM 的几项实验,我们比较了人机协作系统与大型语言模型在模式提取任务中的表现。我们在 DaSH@EMNLP 2022 上展示了这项工作,你可以在 这里找到论文。
具体来说,我们研究了以下工具:
-
模式归纳 是 IBM Watson Discovery 上用于文本模式提取的 HITL 工具。
-
GPT-3 是一个流行的大型生成语言模型。GPT-3 是最大的语言模型之一,拥有 1750 亿个参数。
我们比较工作的目标。图像由作者提供。
文本模式提取
什么是文本模式提取?下面,我们有一个场景,其中包含一系列财务新闻稿,我们需要提取财政时间段。我们希望的提取结果包括“2014 年第一季度”和“2013 年第四季度”。注意,财政时间段的年份可以出现在提取结果的开始或结束,而季度则可以是“第一”、“第二”、“第三”或“第四”。
文本模式提取。图像由作者提供。
文本模式提取与模式归纳
模式归纳在 IBM Watson Discovery 上可用。它是一个用于文本模式提取的 HITL 工具。终端用户通过提供反馈来提取文本模式。该工具无需任何编码,用户也不需要提供大规模的训练数据集。
模式归纳支持两种用户操作:
-
终端用户高亮他们希望提取的文本示例。
-
终端用户还会向系统的提取结果提供反馈。用户可以接受或拒绝这些提取结果。
在我们的实验中,我们通过模拟两种用户操作来进行用户模拟:(1) 高亮示例文本,和 (2) 提供反馈。
通过模拟用户操作来评估 HITL 工具。图像作者提供。
在后台,模式诱导学习 提取规则。可以将它们视为类似于正则表达式的东西:描述一系列标记或单词模式的表达式。
模式诱导流程及其基础规则模型。图像作者提供。
GPT-3 的文本模式提取
虽然最终用户会在模式诱导中提供文本高亮和反馈,但在 GPT-3 中完成文本提取任务需要构造输入提示,而 GPT-3 输出一个 完成文本,我们期望其中包含提取的文本。
基本的 GPT-3 文本模式提取提示。图像作者提供。
假设我们需要从报告数据集中提取 ISO 编号,如“ISO 18788”或“ISO 223000”。在上面的图像中,我们有一个按照以下方式制作的输入提示:
-
输入提示的第一部分是我们要提取的来自数据集的句子。我们将此句子放在方括号中。
-
提示的第二部分包含以列表形式列出的示例提取,其中每个示例提取都放在竖线字符“|”之间。
输入提示的格式用于向 GPT-3 显示和演示我们希望从句子中提取哪些类型的文本。在上面的例子中,GPT-3 从句子中完成了文本,输出了“ISO 9001”。上述输入提示是一种相当天真的方法,它模仿了最终用户可能构造提示的方式。构造提示需要一定程度的工程技术,整个研究子领域都致力于提示工程。
提示
尝试了几种不同的输入格式。图像作者提供。
输入提示的格式有很多种。我们尝试了几种格式:
-
基本提示:模仿可能没有太多技术专长的最终用户如何构造提示。
-
结构化提示:不是列出提取示例,而是将每个提取示例与一个句子配对。
结构化提示。图像作者提供。
- 带有附加和负面示例的结构化提示: 在最后一种提示中,我们添加了附加示例和负面示例。在模式诱导中,用户可以接受和拒绝提取。在 GPT-3 中,我们将这些类型的提取作为附加示例添加。
带有负面示例的结构化提示。图像作者提供。
要了解有关输入提示格式的更多细节,请参阅 论文。
后处理 GPT-3 的输出
使用 GPT-3 进行文本模式提取的主要挑战之一是它有时会对输出进行创意性的处理。这是 GPT-3 常见的现象,考虑到其文本生成能力。GPT-3 提取的文本并不总是文档数据集的一部分。
GPT-3 输出的文本有创意。图片由作者提供。
在一个用例中,我们希望从犯罪报告中提取犯罪事件的百分比,如“财产犯罪”或“人身犯罪”的百分比。但 GPT-3 生成了有关其他主题的百分比,如种族或性别,例如“0.6%为美洲印第安人或阿拉斯加土著”。这些文本甚至没有出现在数据集中,这对精确度评分产生了负面影响。因此,我们对 GPT-3 的所有输出进行了后处理。后处理步骤包括清理输出文本(去除分隔符)和删除那些不属于文档数据集的输出。
实验设置
这是我们用于将 GPT-3 与模式归纳进行比较的实验设置的详细信息:
用户模拟实验设置。图片由作者提供。
用户模拟在模式归纳和 GPT-3 上运行。每个工具都进行了 7 个用例任务:
我们给 GPT-3 和模式归纳的用例任务。图片由作者提供。
用户模拟记录了每次运行的精确度和召回率。
GPT-3 实验
图片由作者提供。
在对 GPT-3 的评估过程中,我们使用了用户模拟将作为高亮文本的相同种子示例,种子示例取自模式归纳用户模拟的日志,然后这些种子示例被用作 GPT-3 的输入提示。这是为了使其与模式归纳的运行结果可比。由于 GPT-3 的输入提示有令牌数量限制(约 4K 个令牌),我们还将文档拆分成部分。
结果:精确度评分
我们计算了平均精确度,这些精确度是对每个 7 个用例中的 100 次用户模拟运行的综合结果。结果显示 GPT-3 的精确度评分低于模式归纳的精确度评分(绿色线条带三角标记):
GPT-3 的精确度评分低于模式归纳。图片由作者提供。
图表中有几条 GPT-3 的线,每条线属于某种不同的输入提示格式变体。虽然不同的输入提示格式提高了精确度评分,但 GPT-3 的精确度评分平均值并没有超过 HITL 的精确度评分。总体而言,在精确度方面,模式归纳的表现比最佳 GPT-3 模型(结构化提示和额外示例)平均高出 38.8%。
结果:召回率评分
我们还计算了平均召回率,即对每个 7 个用例任务的 100 次用户模拟运行的汇总。
GPT-3 的召回分数相似且更高。图片来自作者。
总体而言,在召回率方面,模式诱导的表现比最好的 GPT-3 模型平均好 4.0%。 然而,当对每个用例任务分析召回分数时,我们注意到 GPT-3 的运行在召回分数上要么相似,要么甚至高于模式诱导的运行:
- 任务 U1、U3、U4:在这些任务中,预期的提取不遵循某些语法模式。例如,任务 U3 需要提取不同类型的犯罪,例如“财产犯罪”、“人身犯罪”。或者,任务 U4 需要提取整数和分数类型的量杯。这些任务提取的是概念,GPT-3 在这些任务中表现得相当好。GPT-3 理解某些词指代概念和实体,例如国家、犯罪类型或整数和分数量。GPT-3 对文本中的概念理解得非常好。
GPT-3 对文本中的概念理解得非常好。图片来自作者。
- 任务 U2、U5、U6、U7:另一方面,我们看到,具有更严格语法模式的任务在模式诱导中处理得更好。例如,在任务 U2 中,我们希望提取事件的计数。字面上的“事件”通常出现在一个 6 位整数的末尾。这个模式适用于所有预期的提取。这些任务更具语法性,模式诱导在处理具有更严格语法模式的提取任务时表现更好。 这很合理,因为底层的模式诱导模型是基于规则的。
模式诱导更好地学习严格模式。图片来自作者。
改进提示格式提高了召回分数
我们在 GPT-3 运行中观察到的另一个关键点是,改进输入提示格式会提高召回分数。
结构化提示提高了召回率。图片来自作者。
从上面的图表可以看出,从基础提示分数(橙色线)到结构化提示分数(黄色线),召回分数平均提高了 59.8%。这张图的关键结论是,提示工程和格式化对于提高召回分数非常重要。
比较分析总结
图片来自作者。
HITL 方法用于文本模式提取的结果具有更高的精度,适合那些没有太多技术背景的最终用户。此外,有两个方面在 GPT-3 中找不到,但在 HITL 中存在:
-
HITL 方法能够引出有针对性的用户反馈,并
-
这允许采用迭代方法来构建模型。
然而,HITL 方法的召回率较低,并且更适合处理句法文本模式。
在大型生成模型中,给定结构化的提示和后处理步骤,GPT-3 提供了更高的召回率。GPT-3 能够对提示进行上下文化并学习更通用的模型。然而,GPT-3 在文本模式提取中的缺点是,它本身无法像 HITL 方法那样执行提取任务。实际上,为了获得与 HITL 模型相当的结果,我们不得不
-
设计并构建提示的结构,以利用 GPT-3 强大的语言能力和
-
对 GPT-3 的字符串输出进行后处理。
然而,这些步骤可能不会被没有太多技术培训的终端用户采取。
结论与未来工作
在我们的初步工作中,我们比较了 HITL 和预训练的大型生成语言模型在文本模式提取任务中的表现。我们想了解终端用户如何使用广泛研究的模型进行文本模式提取。我们发现,HITL 方法在精确度上平均表现更好,而 GPT-3 在召回率上具有相当或更高的得分。
我们的未来工作在这些结果的基础上进行。我们如何结合 HITL 和预训练的大型生成语言模型的优点? 我们如何利用用户输入来改进提示设计,并进而利用大型语言模型的上下文和语言能力?
文本搜索与向量搜索:更好地结合?
了解如何使用 OpenSearch 设置混合搜索系统,以便您可以同时受益于文本搜索和向量搜索的优势
·
关注 发表在 数据科学前沿 ·8 分钟阅读·2023 年 2 月 16 日
--
图片由 Aarón Blanco Tejedor 提供,来源于 Unsplash
文本数据库在许多业务工作负载中扮演着关键角色,特别是在电子商务中,客户依赖产品描述和评论来做出明智的购买决定。向量搜索,利用文本嵌入来找到语义相似的文档,是另一个强大的工具。然而,由于对将其实施到现有工作流程的复杂性的担忧,一些企业可能会对尝试向量搜索持保留态度。但是,如果我告诉你这可以很容易地完成并且带来显著的好处呢?
在这篇博客文章中,我将展示如何轻松创建一个结合文本和向量搜索的混合设置。这个设置将给你最全面和准确的搜索结果。我将使用 OpenSearch 作为搜索引擎,并使用 Hugging Face 的 Sentence Transformers 来生成嵌入。我为这个任务选择的数据集是“XMarket”数据集(更详细的描述见 这里),在索引过程中,我们将标题字段嵌入到向量表示中。
准备数据集
首先,我们将使用 Sentence Transformers 来索引我们的文档。这个库有预训练的模型,可以生成句子或段落的嵌入。这些嵌入作为文本的独特指纹。在索引过程中,我将标题字段转换为向量表示,并将其在 OpenSearch 中索引。你可以通过简单地导入模型并编码任何文本字段来实现这一点。
可以通过编写以下两行来导入模型:
from sentence_transformers import SentenceTransformer
model = SentenceTransformer('sentence-transformers/all-MiniLM-L6-v2')
embedding = model.encode(text_field)
就这么简单!
我们将通过传递以下映射来创建一个名为“products”的索引:
{
"products":{
"mappings":{
"properties":{
"asin":{
"type":"keyword"
},
"description_vector":{
"type":"knn_vector",
"dimension":384
},
"item_image":{
"type":"keyword"
},
"text_field":{
"type":"text",
"fields":{
"keyword_field":{
"type":"keyword"
}
},
"analyzer":"standard"
}
}
}
}
}
asin — 从产品元数据中获取的文档唯一 ID。
description_vector — 这是我们存储编码后的产品标题字段的地方。
item_image- 这是产品的图片网址
text_field — 这是产品的标题
请注意,我们使用的是标准的 OpenSearch 分析器,它会将字段中的每个单词标记为单个关键词。OpenSearch 使用这些关键词来进行 Okapi BM25 算法。我还将标题字段保存了两次;一次是其原始格式,一次是向量表示。
然后,我将使用该模型来编码标题字段,并创建将批量上传到 OpenSearch 的文档:
def store_index(index_name: str, data: np.array, metadata: list, os_client: OpenSearch):
documents = []
for index_num, vector in enumerate(data):
metadata_line = metadata[index_num]
text_field = metadata_line["title"]
embedding = model.encode(text_field)
norm_text_vector_np = normalize_data(embedding)
document = {
"_index": index_name,
"_id": index_num,
"asin": metadata_line["asin"],
"description_vector": norm_text_vector_np.tolist(),
"item_image": metadata_line["imgUrl"],
"text_field": text_field
}
documents.append(document)
if index_num % 1000 == 0 or index_num == len(data):
helpers.bulk(os_client, documents, request_timeout=1800)
documents = []
print(f"bulk {index_num} indexed successfully")
os_client.indices.refresh(INDEX_NAME)
os_client.indices.refresh(INDEX_NAME)
混合搜索实现
计划是创建一个客户端,该客户端将从用户那里获取输入,使用 Sentence Transformers 模型生成嵌入,并执行我们的混合搜索。用户还会被要求提供一个提升级别,即他们希望给予文本搜索或向量搜索的相对重要性。这样,用户可以选择优先考虑一种搜索类型。例如,如果用户希望他的查询的语义意义比描述中的简单文本出现更重要,他可以给向量搜索更高的提升等级。
搜索
我们将首先使用 OpenSearch 的搜索方法在索引上进行文本搜索。此方法接受一个查询字符串,并返回与查询匹配的文档列表。OpenSearch 通过利用 Okapi BM25 作为排名算法来获取文本搜索结果。使用 OpenSearch 进行文本搜索是通过发送以下请求体进行的:
bm25_query = {
"size": 20,
"query": {
"match": {
"text_field": query
}
},
"_source": ["asin", "text_field", "item_image"],
}
其中 textual_query 是用户输入的文本。为了使我的结果以干净的方式返回,我添加了“_source”,以便 OpenSearch 仅返回我感兴趣的特定字段。
由于文本和向量搜索的排名分数算法不同,我们需要将分数调整到相同的尺度,以便结合结果。为此,我们将对每个文档的文本搜索分数进行归一化。最大 BM25 分数是针对特定查询在集合中分配给文档的最高分数。它表示文档与查询的最大相关性。最大 BM25 分数的值取决于 BM25 公式的参数,例如平均文档长度、术语频率和逆文档频率。因此,我取了 OpenSearch 为每个查询收到的最大分数,并将每个结果分数除以它,得到 0 到 1 之间的分数。以下函数演示了我们的归一化算法:
def normalize_bm25_formula(score, max_score):
return score / max_score
接下来,我们将使用向量搜索方法进行向量搜索。此方法接受一个嵌入列表,并返回与这些嵌入在语义上相似的文档列表。
对 OpenSearch 的搜索查询如下所示:
cpu_request_body = {
"size": 20,
"query": {
"script_score": {
"query": {
"match_all": {}
},
"script": {
"source": "knn_score",
"lang": "knn",
"params": {
"field": "description_vector",
"query_value": get_vector_sentence_transformers(query).tolist(),
"space_type": "cosinesimil"
}
}
}
},
"_source": ["asin", "text_field", "item_image"],
}
其中 get_vector_sentence_transformers 将文本发送到 model.encode(text_input),该方法返回文本的向量表示。还要注意,您的 topK 结果越高,结果越准确,但这也会增加延迟。
插值结果并应用提升
由 Amol Tyagi 提供的照片,来源于 Unsplash
现在我们需要将两个搜索结果结合起来。为此,我们将对结果进行插值处理,使得在两个搜索中都出现的每个文档在混合结果列表中排名更高。这样,我们可以利用文本和向量搜索的优势,获得最全面的结果。
以下函数用于插值关键词搜索和向量搜索的结果。它返回一个字典,包含两个命中的结果集之间的共同元素及每个文档的分数。如果文档只出现在一个搜索结果中,则我们将其分配为检索到的最低分数。
def interpolate_results(vector_hits, bm25_hits):
# gather all product ids
bm25_ids_list = []
vector_ids_list = []
for hit in bm25_hits:
bm25_ids_list.append(hit["_source"]["asin"])
for hit in vector_hits:
vector_ids_list.append(hit["_source"]["asin"])
# find common product ids
common_results = set(bm25_ids_list) & set(vector_ids_list)
results_dictionary = dict((key, []) for key in common_results)
for common_result in common_results:
for index, vector_hit in enumerate(vector_hits):
if vector_hit["_source"]["asin"] == common_result:
results_dictionary[common_result].append(vector_hit["_score"])
for index, BM_hit in enumerate(bm25_hits):
if BM_hit["_source"]["asin"] == common_result:
results_dictionary[common_result].append(BM_hit["_score"])
min_value = get_min_score(common_results, results_dictionary)
# assign minimum value scores for all unique results
for vector_hit in vector_hits:
if vector_hit["_source"]["asin"] not in common_results:
new_scored_element_id = vector_hit["_source"]["asin"]
results_dictionary[new_scored_element_id] = [min_value]
for BM_hit in bm25_hits:
if BM_hit["_source"]["asin"] not in common_results:
new_scored_element_id = BM_hit["_source"]["asin"]
results_dictionary[new_scored_element_id] = [min_value]
return results_dictionary
最终我们将得到一个以文档 ID 为键,以分数数组为值的字典。数组中的第一个元素是向量搜索分数,第二个元素是文本搜索归一化分数。
最后,我们对搜索结果应用提升。我们将遍历结果的分数,将第一个元素乘以向量提升水平,第二个元素乘以文本提升水平。
def apply_boost(combined_results, vector_boost_level, bm25_boost_level):
for element in combined_results:
if len(combined_results[element]) == 1:
combined_results[element] = combined_results[element][0] * vector_boost_level + \
combined_results[element][0] * bm25_boost_level
else:
combined_results[element] = combined_results[element][0] * vector_boost_level + \
combined_results[element][1] * bm25_boost_level
#sort the results based on the new scores
sorted_results = [k for k, v in sorted(combined_results.items(), key=lambda item: item[1], reverse=True)]
return sorted_results
现在是时候看看我们有什么了!这就是完整的工作流程:
作者制作的 GIF
我搜索了一个句子“冰淇淋勺”,为向量搜索和文本搜索分别设置了 0.5 的提升,这就是我在前几个结果中得到的:
向量搜索返回 —
来自 XMarket 数据集的图片
文本搜索返回 —
来自 XMarket 数据集的图片
混合搜索返回 —
来自 XMarket 数据集的图片
在这个例子中,我们使用文本和向量搜索来搜索“冰淇淋勺”。文本搜索返回包含关键词“an”、“ice”、“cream”和“scoop”的文档。文本搜索排名第四的结果是一个冰淇淋机,它显然不是一个勺子。它排名如此靠前的原因是其标题“Breville BCI600XL Smart Scoop Ice Cream Maker”包含了句子中的三个关键词:“Scoop”、“Ice”、“Cream”,因此在 BM25 中的评分很高,尽管它与我们的搜索不匹配。而向量搜索则返回语义上与查询相似的结果,无论关键词是否出现在文档中。它知道“scoop”出现在“ice cream”之前意味着匹配度较低。因此,我们得到了一个更全面的结果集,其中包含了比单纯提到“冰淇淋勺”的文档更多的信息。
很明显,如果你只使用一种搜索方式,你将错过有价值的结果或显示不准确的结果,从而使客户感到沮丧。当利用两者的优势时,我们会得到更准确的结果。所以,我相信我们的答案是,"更好地结合"已经证明是正确的。
但等等,更好的可以变得更好吗?改善搜索体验的一种方法是利用 OpenSearch 中的 APU 力量(联想处理单元)。通过使用 Searchium.ai 的插件在 APU 上进行向量搜索,我们可以利用先进的算法和处理能力来进一步改善延迟并显著降低成本(例如,$0.23 对比 $8.76),同时仍然获得类似的结果。
我们可以安装插件、将索引上传到 APU,并通过发送略微修改过的请求体进行搜索:
apu_request_body = {
"size": 20,
"query": {
"gsi_knn": {
"field": "description_vector",
"vector": get_vector_sentence_transformers(query).tolist(),
}
},
"_source": ["asin", "text_field", "item_image"],
}
其他所有步骤都是相同的!
总结一下,通过结合使用 OpenSearch 和 Sentence Transformers 的文本与向量搜索,企业可以轻松改善搜索结果。而通过利用 APU,企业可以将搜索结果提升到一个新的水平,同时降低基础设施成本。不要让复杂性的问题阻碍你。试一试,看看它能带来哪些好处。祝搜索愉快!
完整的代码可以在这里找到。
特别感谢Yaniv Vaknin 和Daphna Idelson的所有帮助!
文本切分正确实施:为您的个人 LLM 打下坚实的基础
如何从头开始使用语义和词汇相似性构建文本切分模型
·
关注 发表于 Towards Data Science · 11 分钟阅读 · 2023 年 6 月 4 日
--
图片由 Gary Butterfield 提供,来自 Unsplash
现在似乎每个人都在尝试获得自己的大型语言模型(LLM),并调整其以适应他们的私人文档集。隐私因素在这里起着重要作用,进一步推动了对更多私人 GPT 模型的需求。然而,创建个人聊天机器人的过程并不简单,你基本上有两个主要选项来实现这一目标。
首先,你可以从头开始构建一个定制的问题-答案数据集,并用它来微调你的 LLM。但说实话,由于高成本和显著的时间投入,这对大多数人来说并不是一个可行的选项。另一种更具成本效益的方法是动态生成上下文。这是通过基于用户查询从文档中检索相关部分来完成的,借助嵌入技术。尽管有很多教程解释如何做这件事,但很少有人强调适当地切分或“切块”文档的重要性。
这之所以至关重要,是因为如果你的文档切分不准确,你的上下文可能会出现偏差,从而导致你的 LLM 给出的答案完全偏离主题,或者更糟的是,生成虚假的信息——在机器学习中,这种现象通常被称为“幻觉”。这就是文本切分艺术发挥作用的地方。这一过程的核心在于将文档拆分成连贯且有意义的块,以便于精确、相关的上下文检索。这样做,你很可能会提高 LLM 的整体表现,使其更擅长理解查询并提供准确的回应。
现在,可能会让你感到惊讶(就像我一样惊讶)的是,在 Python 编程的世界里,文本切分的选项并不多。我们主要的工具是nltk.tokenize.texttiling,但这个工具的文档并不完善。意识到这一点的缺乏和改进的潜力,我决定开始开发自己的文本切分模型,利用自然语言处理(NLP)和变换器提供的革命性技术。
文本切分模型的评估机制
每当我开始开发一个新模型时,我总是尝试以最终目标为出发点,然后从那里向后工作。在这种情况下,我们的“终点”是评估模型的输出。如果没有评估手段,我们就无法衡量性能,因此无法进行改进。因此,在尝试开发模型之前,创建一个评估机制是至关重要的。然而,评估文本切分面临独特的挑战,因为它涉及到文档中出现的主题。这给我们带来了两个主要难题:
-
我们没有带有对应切分的数据集。
-
即使我们有这样的数据集,由于按主题划分文档高度主观,利用起来也会异常困难。
为了应对这些问题,我们将采取一种简单的方法:创建一个合成文档。这个文档将是各种文档的拼接,确保我们知道原始文档之间的确切阈值。这些阈值应由我们的模型识别。在这篇文章中,我将用一个文档作为示例(可以在这里找到)。不过,这种方法也可以用于组装大量文档进行全面的模型测试。这个复合文档是由以下 Medium 文章拼接而成的(对于这些文章的作者,算是免费推广,稍后可以感谢我😀):
3 个生成型 AI 的令人不快的后果 [## 生成型 AI 的 3 个令人不快的后果
‘快速行动,打破常规’的标准操作程序正处于极速运转中。各行各业都在…
平均合同价值在 SaaS [## 平均合同价值在 SaaS
平均合同价值是 SaaS 中的一个重要指标。如果它在上升并趋向右侧,这表明客户…
平均合同价值在 SaaS [## 我们治疗压力、焦虑和抑郁的方法完全错了
心理健康问题的最佳疗法显而易见,但很少被开处方
我们治疗压力、焦虑和抑郁的方法完全错了 [## 你的副业不如在大公司工作
尽管你获得了所有的自由,但在自己工作时你错过了许多东西
现在我们已经建立了评估模型的方法,我们需要定义如何衡量其效能。首先考虑我们的合成文档,其中有预先知道的明确阈值。在我们的例子中,这些阈值为:(0, 56, 74, 118, 163)。这意味着第一篇文章在 56 句话后结束,第二篇在 74 句话后结束,以此类推。我们的模型将根据它在每篇文章中识别的子主题,输出一个类似但更详细的阈值列表。一个示例输出可能是:(0, 26, 54, 67, 74, 90, 112, 120, 130, 163)。
那么,我们如何评估模型的有效性?我能设计出的最合乎逻辑的方法是计算原始向量与模型输出之间的“编辑距离”。这个过程如下:
-
确定模型输出中所有接近真实阈值的数字(排除第一个和最后一个)。使用上面的例子,我们将得到(54, 74, 120)。
-
如果数字少于真实阈值,则用‘None’填补空白(在我们的例子中不会发生)。
-
计算每个相应阈值之间的距离,遇到‘None’时用原始向量的最大值代替。在我们的例子中,这将生成(2, 0, 2)。
-
将这些距离求和,并通过将其除以原始向量的最大阈值乘以向量的长度来标准化。这提供了一个从 0 到 1 的距离,可以通过计算:1-距离来轻松转换为分数。在我们的例子中:1-4/163 → 1-0.0245 → 0.975
-
(可选)根据阈值总数对分数施加惩罚。这是为了惩罚生成过多阈值的模型。尽管这些阈值在统计上可能接近真实值,但它们不一定有意义。
这里是实现上述步骤中描述的评分计算的code
。
这个函数目前远未完美。具体来说,它倾向于产生偏向 1 的值。然而,它足以用于比较我们模型生成的结果。如果你有任何改进其实现的建议,我非常乐意在评论中听取。
生成句子相似度分数
现在我们已经建立了评估模型性能的方法,我们可以开始考虑如何提取阈值。我们模型的基本概念相当简单:文档的块本质上是具有某种相似性水平的句子簇。换句话说,位于同一块中的句子对应该产生较高的相似度分数,而来自不同块的句子对应该产生较低的分数。无论我们决定使用哪种聚类方法,我们都可以安全地说,我们需要一种句子相似度度量。在自然语言处理(NLP)中,最常见的两种相似度度量形式是词汇(基于词汇比较)和语义(基于意义,更技术性地基于嵌入)。在我们的模型中,我们将测试不同类型的相似度分数,并比较它们在测试文档上的表现。具体而言,我们将利用以下模型/算法:
-
BERT 分数(语义)
-
SequenceMatcher(词汇)
-
Jaccard 相似度(词汇)
计算相似度分数的函数本质上是一个大型的“IF-ELSE”代码块,根据用户选择的输入模型在不同方法之间切换。
为了性能优化,该函数接受两个句子列表作为输入,并返回相应的相似度度量列表。
接下来,我们需要决定实际要计算哪些相似度分数。一个方法是通过比较每个句子与所有其他句子的对比来计算逐对相似度。这种方法虽然全面,但不仅效率低下且难以扩展,而且也不理想。我们所寻求的簇将由连续的句子组成,这意味着我们不希望识别文档中相距较远的句子之间的联系——实际上,我们的目标是避免这种情况!在另一端,我们可以考虑仅计算给定句子与其后面句子之间的相似度。虽然合理地假设相邻句子具有相同的意义,但这种方法也存在风险。考虑“填充句子”——那些用于修饰文本但不传达任何特定意义或对上下文没有贡献的句子。例子包括“我会尝试不同的说法”或“但是我跑题了”(出现在我们的测试文档中!)。这样的句子可能会产生人为的阈值,我们肯定想要避免这种情况。
因此,我们将采用一种混合方法,只计算每个句子与接下来的 K 个句子的相似度(是的,K 是一个超参数)。如下面的图示所示,一旦设置了 K 参数,每个句子将连接到 2*K 个句子——K 个前面的句子和 K 个后面的句子。
作者提供的图片。
生成这种相似度分数的代码封装在函数create_similarity_graph
中:
正如函数名称所示,这个函数构建的是一个图,其中节点代表句子,相似性分数作为边的权重。输出格式如下:
(句子 1,句子 2,相似度比 1)
(句子 1,句子 3,相似度比 2)
…
(句子 1,句子 K,相似度比 K-1)
(句子 2,句子 3,相似度比 K)
…
确实,这本质上是一个形式为(父节点,子节点,边权重)的图。还需要注意第 27 行的系数math.exp(-l/2)
,它会乘以相似性分数。我们使用这个系数来适应“距离效应”——即两个句子之间的相似性应随着它们的距离增加而减少。
基于图的文本分割
有了我们的相似度分数,下一步是找到有效的方式对其进行聚类。当前数据的图结构暗示了选择合适算法的方向。虽然有许多图聚类的选项,但我特别偏爱Louvain 社区检测方法,主要是因为它在 Python 中的实现简单且能够高效处理大规模数据。
在算法的上下文中,“社区”指的是一个节点密集互连的簇,同时与其他社区的节点连接稀疏。将这个概念转化到我们的文档分块任务中,这些“社区”就是我们寻求的块。每个块或社区是一个高度相关的句子簇,形成文档中的一个连贯主题或子主题。
鉴于上述图结构,提取社区仅需几行代码。
虽然 Louvain 算法在寻找图中的社区方面表现出色,但重要的是要记住,这些社区在文档分块的上下文中可能并不总是对应于连贯的句子序列。这是因为该算法本身并不意识到它处理的是一个文本文档,其中句子的顺序和连续性很重要。
在社区检测过程中,Louvain 算法可能会生成包含非连续句子的聚类。例如,它可能会生成一个类似(1,2,3,4,6,7)的聚类,遗漏了句子 5。虽然这个聚类可能在内部相似性上仍然很高,但它并未在文档中形成一个逻辑上的块,因为从句子 4 到句子 6 有一个“间隙”或“跳跃”。在将基于图的聚类应用于文档分块时,这是一个关键点。我们的期望是找到连贯、不间断的文本段落——即代表相关内容连续块的块。
为了解决这个问题,我在代码中加入了一个后处理步骤,特别是在第 46 行调用 compact_clusters
函数。由于我没有找到任何现成的算法来执行此任务(如果你知道一个,我很乐意了解),我设计了一个基于以下步骤的简单算法:
-
对于每一对聚类,识别它们之间的重叠范围。考虑到聚类 a=(1, 2, 3, 6, 7, 8) 和 b=(4, 5, 9, 10, 11),重叠范围将是 (4, 5, 6, 7, 8)。
-
确定消除重叠所需的最少移动次数。在我们的示例中,我们可以将 (6, 7, 8) 从聚类 ‘a’ 转移到 ‘b’,或者将 (4, 5) 从聚类 ‘b’ 移动到 ‘a’。最优选择是后者,因为需要的移动次数更少。如果出现平局,可以做出随机决策。
-
根据此调整重新配置聚类。调整后,我们将得到 a = (1, 2, 3, 4, 5, 6, 7, 8) 和 b = (9, 10, 11)。
这种方法确保了我们生成的最终瓦片不仅在内部是一致的,而且在原始文档序列的上下文中也是有意义的。
这是函数的实现:
汇总
现在我们已经构建了所有必要的函数,编写一个确定目标文档瓦片的脚本是很简单的任务。只需几行代码:
如前所述,我们仅测试了四种模型,但集成更多模型只需通过添加新的 ‘elif’ 部分来修改 get_similarity_scores
函数。鉴于我们问题的性质,构建计算瓦片的可视化表示也是很有启发性的。这种图形描述提供了一个立即了解我们算法相对于原始文档表现的方式。这是我使用的绘图函数:
以下是结果图:
图片来源于作者。
第一条柱状图展示了原始文档,分割成四篇不同的文章。可以明显看出,BERT Score 的表现非常出色,完美匹配了所有三个主要阈值,而 paraphrase-MiniLM-L6-v2 在三个阈值中有两个未能匹配(第二个几乎匹配,第三个则偏差较大)。值得注意的是,这两个语义模型在文章中识别出了非常相似的子主题,暗示了可以使用集成方法来确定实际应用中的准确阈值。令人惊讶的是,词汇模型的表现并不差,尽管 Jaccard 引入了一些与文档结构不相关的虚假阈值。
总结一下,以下是四种测试算法的得分:
-
BERT Score: 0.9950
-
paraphrase-MiniLM-L6-v2: 0.9321
-
SequenceMatcher: 0.9208
-
Jaccard: 0.9830
收获与下一步
根据我们涉及目标文档的简短测试,很明显,提出的文档标题方法展示了显著的前景。正如我们预期的那样,由于语义方法能够捕捉文本中更深层次的上下文关系,因此在这个特定任务中相比词汇方法具有优势。本文中解释的工作代码的代码库可在此处找到。可能的改进领域包括:
-
评分函数的优化:当前的标题评分函数表现出对值 1 的偏倚。解决这一偏倚,使评分函数更加平衡,将提高结果的可靠性,并提供对模型性能的更准确评估。
-
探索额外的模型:本研究中我们只测试了四种模型。测试更多的模型,特别是不同类型的语义模型,可能会揭示新的见解,并进一步提高性能。这也可以包括尝试结合多个模型优点的集成方法。
-
跨多个文档的验证:我们的测试仅涉及一个文档。对各种文档进行性能评估将使我们更清楚其稳健性和普遍性。不同类型的文本、体裁或主题可能会影响标题生成过程的表现。
-
子主题识别的提升:尽管我们的模型能够识别文章中的子主题,但仍有改进的空间。可以使用集成方法或其他高级策略来提高子主题确定的准确性,确保衍生的标题反映出文档的细致结构。
你喜欢这篇文章吗?如果你对人工智能、自然语言处理、机器学习和数据分析在解决实际问题中的应用感兴趣,你可能也会喜欢我的其他作品。我的目标是创作能够展示这些变革性技术在实际场景中的可操作性文章。如果这也是你的兴趣,关注我在 Medium 上的最新作品吧!
文本新颖性检测
如何使用最小协方差判别法(MCD)检测新颖的新闻头条
·
关注 发表在 Towards Data Science · 8 分钟阅读 · 2023 年 10 月 2 日
--
图片由 Ali Shah Lakhani 提供,发布于 Unsplash。
在今天的信息时代,我们每天都被新闻文章淹没。这些文章中的许多只是对相同事实的重复陈述,但也有一些包含真正的新信息,这些信息可能会对我们的决策产生重大影响。例如,想要投资 Meta 的人可能希望关注那些包含独家信息的文章,而不是那些仅仅重复先前发布数据的文章。能够区分新颖的新闻和冗余的新闻至关重要,这样我们才能在面对信息洪流时做出明智的决策。
这就是新颖性检测发挥作用的地方。新颖性检测是识别新数据或未知数据的任务,这些数据与以前见过的数据有所不同。这是一种无监督学习技术,用于检测数据中的异常、离群值或新模式。关键思想是建立一个“正常”数据的模型,然后利用该模型识别偏离正常的数据点。
在新闻文章的背景下,这涉及到检测文章是否包含在其他地方不可获得的新信息。为此,我们可以开发一个已知或可用的信息基线,然后将新信息与该基线进行比较。如果新信息与基线之间存在显著差异,则可以认为该信息是新颖的。
最小协方差行列式(MCD)
最小协方差行列式(MCD)方法是一种估计数据集协方差矩阵的技术。它可以用来创建一个包围高斯分布中心模式的椭圆形,任何位于该形状外的数据点都可以被视为新颖性(有时称为异常值)。MCD 方法对于噪声大或含有离群值的数据集特别有用,因为它可以帮助识别那些可能不符合整体数据模式的异常数据点。(见示例)
MCD 可以用来检测新闻头条中的新颖性。虽然该方法可以推广到完整文章中,我们的目标是提供一个简明的示例,展示如何在短文本中应用 MCD 进行新颖性检测。MCD 是一个多变量位置和散布的鲁棒估计量,使其非常适合在高维数据(如文本)中识别离群值。在新闻头条的数据集上,MCD 将基于协方差学习一个“正常”头条的模型。然后我们可以使用该模型来评分新头条,并标记那些显著偏离正常的头条,作为潜在的新颖或异常故事。示例代码和实验将说明 MCD 新颖性检测在实践中的运作方式。
步骤方法
嵌入: 在机器学习中,我们使用嵌入作为一种更紧凑和高效的方式来表示数据。嵌入将原始数据转换为捕捉数据最重要特征的低维表示。
文本嵌入是一种特定类型的嵌入,用于将文本数据转换为向量表示。它考虑了单词、短语和句子之间的语义和关系,并将它们转换为捕捉文本含义的数值表示。这使我们能够执行诸如查找相似文本、基于语义意义对文本进行聚类等操作。
假设我们收集了过去几个月有关 Meta 的以下头条新闻:
news = [
"Mark Zuckerberg touts potential of remote work in metaverse as Meta threatens employees for violating return-to-office mandate",
"Meta Quest 3 Shows Us the Metaverse Dream isn’t Dead Yet",
"Meta has Apple to thank for giving its annual VR conference added sizzle this year",
"Meta launches AI chatbots for Instagram, Facebook and WhatsApp",
"Meta Launches AI Chatbots for Snoop Dogg, MrBeast, Tom Brady, Kendall Jenner, Charli D’Amelio and More",
"Llama 2: why is Meta releasing open-source AI model and are there any risks?",
"Meta's Mandatory Return to Office Is 'a Mess'",
"Meta shares soar on resilient revenue and $40bn in buybacks",
"Facebook suffers fresh setback after EU ruling on use of personal data",
"Facebook owner Meta hit with record €1.2bn fine over EU-US data transfers"
]
我们可以使用 OpenAI 生成每个句子的文本嵌入,方法如下:
def get_embedding(text,
model = 'text-embedding-ada-002'):
text = text.replace("\n", " ")
return openai.Embedding.create(input = [text], engine = model)['data'][0]['embedding']
df['embedding'] = df.news.apply(lambda x: get_embedding(x))
df['embedding'] = df['embedding'].apply(np.array)
matrix = np.vstack(df['embedding'].values)
matrix.shape
# Output: (10, 1536)
来自 OpenAI 的 text-embedding-ada-002
模型 是一个前沿的嵌入模型,它接受一个句子作为输入,并输出长度为 1536 的嵌入向量。该向量表示输入句子的语义含义,可以用于语义相似性、文本分类等任务。该模型的最新版本采用了最先进的语言表示技术,以生成高度准确和鲁棒的嵌入。如果你无法访问 OpenAI,你可以使用其他嵌入模型,如 Sentence Transformers。
一旦生成了嵌入,我们会创建一个矩阵变量,用于存储来自 df[‘embedding’]
列的嵌入矩阵表示。这是通过使用 NumPy
库中的 vstack
函数完成的,该函数将列中的所有向量(每个表示一个句子)垂直堆叠以创建一个矩阵。这使我们能够在下一步中使用矩阵运算。
计算 MCD: 我们使用嵌入作为特征,并计算 MCD 以估计中心数据云(多变量高斯分布的中心模式)的位置和形状。
拟合椭圆包络: 然后,我们使用计算得到的 MCD 拟合一个椭圆包络到中心模式。这个包络作为边界,将正常点与新颖点分开。
预测新颖句子: 最后,我们使用椭圆包络对嵌入进行分类。位于包络内部的点被认为是正常的,而位于包络外部的点被认为是新颖的或异常的。
为了完成这一切,我们使用 Python 中的 scikit-learn
库中的 EllipticEnvelope
类来应用 MCD:
# Reduce the dimensionality of the embeddings to 2D using PCA
pca = PCA(n_components=2)
reduced_matrix = pca.fit_transform(matrix)
reduced_matrix.shape
# Fit the Elliptic Envelope (MCD-based robust estimator)
envelope = EllipticEnvelope(contamination=0.2)
envelope.fit(reduced_matrix)
# Predict the labels of the sentences
labels = envelope.predict(reduced_matrix)
# Find the indices of the novel sentences
novel_indices = np.where(labels == -1)[0]
novel_indices
#Output: array([8, 9])
contamination
是一个参数,你可以根据期望的新颖句子数量进行调整。它表示数据集中异常值的比例。predict
方法返回一个标签数组,其中 1
表示正常点(内点),而 -1
表示异常点(新颖点)。
此外,为了将高维嵌入可视化为 2D 以及节省计算时间,我们使用 PCA 将高维嵌入向量投影到较低维度的 2D 空间,我们将其称为 reduced_matrix
。
我们可以看到 novel_indices
输出了 array([8, 9])
,这是被认为是新颖的句子索引。
绘制结果: 我们可以通过绘制嵌入和椭圆包络来可视化结果。正常点(内点)可以用一种颜色或标记绘制,而异常点(新颖点)可以用另一种颜色或标记绘制。椭圆包络可以通过绘制与马氏距离对应的椭圆来可视化。
为了实现可视化,我们:
-
提取拟合的椭圆包络模型的位置和协方差矩阵。
-
计算协方差矩阵的特征值和特征向量,以确定椭圆的方向和轴长。
-
计算每个样本到拟合椭圆模型中心的马氏距离。
-
根据污染参数确定一个阈值距离,该参数指定了预期的异常值百分比。
-
根据阈值马氏距离缩放椭圆的宽度和高度。
-
将椭圆内部的点标记为内点,外部的点标记为外点。
-
绘制内点和外点,并添加缩放的椭圆补丁。
-
用索引标注每个数据点以识别异常值。
# Extract the location and covariance of the central mode
location = envelope.location_
covariance = envelope.covariance_
# Compute the angle, width, and height of the ellipse
eigenvalues, eigenvectors = np.linalg.eigh(covariance)
order = eigenvalues.argsort()[::-1]
eigenvalues, eigenvectors = eigenvalues[order], eigenvectors[:, order]
vx, vy = eigenvectors[:, 0]
theta = np.arctan2(vy, vx)
# Compute the width and height of the ellipse based on the eigenvalues (variances)
width, height = 2 * np.sqrt(eigenvalues)
# Compute the Mahalanobis distance of the reduced 2D embeddings
mahalanobis_distances = envelope.mahalanobis(reduced_matrix)
# Compute the threshold based on the contamination parameter
threshold = np.percentile(mahalanobis_distances, (1 - envelope.contamination) * 100)
# Scale the width and height of the ellipse based on the Mahalanobis distance threshold
width, height = width * np.sqrt(threshold), height * np.sqrt(threshold)
# Plot the inliers and outliers
inliers = reduced_matrix[labels == 1]
outliers = reduced_matrix[labels == -1]
# Re-plot the inliers and outliers along with the elliptic envelope with annotations
plt.scatter(inliers[:, 0], inliers[:, 1], c='b', label='Inliers')
plt.scatter(outliers[:, 0], outliers[:, 1], c='r', label='Outliers', marker='x')
ellipse = Ellipse(location, width, height, angle=np.degrees(theta), edgecolor='k', facecolor='none')
plt.gca().add_patch(ellipse)
# Annotate each point with its index
for i, (x, y) in enumerate(reduced_matrix):
plt.annotate(str(i), (x, y), textcoords="offset points", xytext=(0, 5), ha='center')
plt.title('Novelty Detection using MCD with Annotations')
plt.xlabel('Feature 1')
plt.ylabel('Feature 2')
plt.legend()
plt.grid(True)
plt.show()
最后,我们得到内点和外点的可视化效果如下:
绘制包络线以及标记为内点或外点的点
现在让我们查看标题,第 8 条和第 9 条是:
Facebook 在欧盟关于个人数据使用的裁决后遭遇新挫折。
Facebook 母公司 Meta 因欧盟-美国数据传输问题被罚创纪录的 12 亿欧元。
这两个标题都与欧盟调节 Meta 在其平台上如何使用和传输个人数据的努力有关。
而内点标题则主要讨论 Meta 如何全力投入人工智能和虚拟现实。人工智能的重点在于新发布的 AI 聊天机器人,而虚拟现实的重点在于新发布的 Meta Quest 3 头戴设备。你还可以注意到第 0 条和第 6 条标题涉及远程办公设置,因此它们在图中的位置较为接近。
总结
在这篇文章中,我们展示了如何根据分布区分正常点和新颖点。简而言之,正常点是指位于数据分布的高密度区域的点,即它们在特征空间中接近大多数其他点。而新颖点则是位于数据分布的低密度区域的点,即它们在特征空间中远离大多数其他点。
在 MCD 和椭圆包络的背景下,正常点是指位于椭圆包络内部的点,椭圆包络是拟合于数据分布的中央模式的。而新颖点则位于椭圆包络外部。
我们还了解到有一些参数影响 MCD 的结果,这些参数包括:
-
阈值: 决策边界或阈值在确定一个点是正常还是新颖方面至关重要。例如,在椭圆包络方法中,位于包络线内部的点被认为是正常的,而那些在包络线外部的点被认为是新颖的。
-
污染参数: 这个参数通常用于新颖性检测方法中,定义了预期为新颖或被污染的数据比例。它影响包络线或阈值的紧密程度,从而影响一个点是否被分类为正常或新颖。
我们还应该注意,对于新的文章,由于每篇新闻文章来自不同的周,新颖性检测方法应考虑新闻的时间因素。如果该方法本身没有考虑时间顺序,你可能需要手动纳入这一方面,例如通过考虑话题或情感的变化,这将超出本帖的范围。
1958 年的感知机作为肿瘤分类器
在 Mathematica 中实现的实际示例
·
关注 发表在Towards Data Science ·14 分钟阅读·2023 年 1 月 3 日
--
美国海军使用 Mark I Perceptron 读取字母。美国海军国家博物馆,1960 年。图片来源于维基共享资源(公共领域)。
介绍
1958 年由弗兰克·罗森布拉特开发的罗森布拉特感知机[1][2],被认为是神经网络的起源,因为它是第一个展示机器从数据中学习能力的算法。
感知机是一个简单的模型,由一层人工神经元或单元组成,这些单元可以被训练来识别输入数据中的模式。这标志着人工智能领域的开始,并为更复杂的神经网络的发展铺平了道路,这些神经网络已被用于各种应用。
第一个实现是 1950 年代末由麻省理工学院林肯实验室开发的 Mark I 感知机。它是第一个能够从示例中学习的机器,用于执行模式识别任务,如读取手写信件和识别口语。Mark I 感知机使用真空管和其他电子组件构建,由一系列按层结构连接的单元组成。每个单元能够处理输入数据,并调整其内部权重和偏置,以识别数据中的模式。
图 1. Mark 1 的细节。来源:维基共享资源。
这台机器在 1960 年代被美国海军用于读取手写信件。那时,海军接收到大量的信件,需要一种快速而准确的处理信件的方法。他们转向了感知机,这种机器能够从示例中学习并识别输入数据中的模式。通过在大量手写信件数据集上训练感知机,海军能够开发出一个能够准确读取和分类信件的系统,且只需最少的人力干预。这在当时是一个重要的成就,因为它展示了人工智能和机器学习在自动化先前由人类完成的任务方面的潜力。感知机在这一应用中的成功帮助确立了这项技术作为一种强大的模式识别工具,并为其在各种应用中的使用打开了大门。
单神经元网络
罗斯伯拉特感知机是一个简单的人工神经元模型。在这个模型中,单个神经元有多个输入,这些输入是输入数据中特征的值,以及一个输出,这是一个二进制值,指示输入数据是否属于两个类别之一。感知机通过为每个输入分配权重来工作,权重表示该输入在确定输出中的重要性。然后,使用一个数学函数来计算输出,这个函数将加权输入与偏置项结合在一起。偏置项是一个可以调整的固定值,用来移动感知机的输出。通过根据预测中的误差调整权重和偏置,感知机可以被训练以识别输入数据中的模式。这种调整权重和偏置以最小化误差的过程称为训练感知机。
图 2. 感知器对应于一个神经元神经网络。在图像中未显示偏置,偏置必须单独添加,作为一个常数应用到一个权重中,或完全不添加。图片来自Chrislb,维基共享资源。
该模型由一个单层单元组成,每个单元具有d个特征作为输入,和一个可以取值为-1和+1的二进制输出。感知器利用这些输入和输出学习如何将数据分类为两个类别。
图 3. 训练观测方程。
为了训练感知器,我们需要一个训练集,其中包含多个观测值,每个观测值包括 d 个特征(X向量)和实际输出(y)。感知器利用这些观测值来学习如何根据特征预测输出。感知器的输出通过权重向量和特征向量的点积的符号计算得出。
预测函数
罗斯布拉特感知器的预测函数用于根据输入数据和单位的内部权重及偏置计算感知器的输出。感知器的输出是一个二进制值,表示输入数据是否属于两个类别之一。预测函数定义如下:
图 4. 无偏置的预测函数。
图 5. 带偏置的预测函数。
其中W是与输入特征对应的权重向量,X是特征的输入值向量。模型可以有偏置或没有偏置,其中b是偏置项。符号函数如果括号中的值为负数,则返回-1,如果为正数,则返回+1。感知器在训练过程中调整权重和偏置,以最小化预测中的误差并提高准确性。
偏置
罗斯布拉特感知器中的偏置项代表了应用于所有输入的额外权重。它用于移动感知器的输出,并可以在训练过程中进行调整以提高模型的准确性。偏置可以通过两种方式之一实现:作为一个常数+1 添加到一个特征中,或作为一个单独调整的外部参数。
如果偏置作为常数+1 特征(特征技巧)实现,它将像任何其他输入特征一样处理,并赋予其自己的权重。这意味着偏置项包含在输出计算中。
另外,偏置也可以作为一个外部参数来实现,与其他权重单独调整。然后将偏置添加到通过权重向量和特征向量计算的输出中。
当特征变量的均值已居中,但二元类预测的均值不为 0 时,Rosenblatt 感知器中的偏置项是有用的,因为它允许模型调整决策边界,以更好地适应数据。当二元类别分布高度不平衡时,这一点尤为重要,因为模型可能倾向于更频繁地预测多数类,以最小化错误。在这种情况下,偏置可以用来调整决策边界的位置,提升模型正确分类少数类的能力。
损失函数
Rosenblatt 感知器模型没有包含损失函数的正式定义,即使其目标是最小化预测值和实际值之间的误差。为了实现这一点,感知器根据预测中的误差调整模型的权重和偏置。定义感知器模型中的误差的一种方法是使用最小二乘法,即最小化预测值和实际值之间平方差的总和。这个损失函数可以用数学形式表示如下:
图 6. 感知器的损失函数。
尽管梯度下降是机器学习中最常用的损失函数最小化方法,但它不能应用于 Rosenblatt 感知器,原因在于该函数不连续。相反,感知器使用了一种称为感知器收敛定理的学习规则,该规则基于调整模型的权重和偏置以最小化预测误差的思想。
图 7. 相当于损失函数的梯度下降。
图 8. 优化权重向量的迭代方程。
Rosenblatt 感知器模型中的学习参数是一个超参数,它决定了学习算法的步长。它用于控制模型权重和偏置根据预测误差的更新速度。较大的学习参数会导致权重和偏置的更新幅度较大,这可能导致更快的学习,但也可能增加过拟合的风险。较小的学习参数会导致较小的更新幅度,这可能导致学习速度较慢,但也可能减少过拟合的风险。
尽管感知机模型是在引入梯度下降概念之前开发的,但通常描述为具有随机梯度下降(SGD)学习算法。这是因为感知机学习规则,即感知机收敛定理,精神上类似于梯度下降,因为它涉及迭代调整模型的权重和偏置,以最小化预测误差。与梯度下降一样,感知机使用学习参数来控制更新的步长,并且可以被视为一种在线学习形式,因为它一次处理一个训练样本。
总的来说,感知机模型中的学习参数在控制学习过程的速度和准确性方面起着至关重要的作用。通过调整学习参数,可以微调感知机模型的性能,并在各种分类任务中获得更好的结果。
感知机在行动中:一个乳腺癌预测器
威斯康星州乳腺癌数据集[3]是一个常用的数据集,用于展示罗森布拉特感知机模型的能力。该数据集包含 1989 年至 1992 年期间拍摄的 699 个乳腺癌活检图像样本,这些样本根据某些特征的存在被分类为良性或恶性。数据集中包括从图像中计算出的 9 个特征,包括肿块厚度、细胞大小的均匀性、细胞形状的均匀性、边缘粘附、单个上皮细胞大小、裸核、平淡的染色质、正常核仁和肿瘤的有丝分裂。
该数据集是观察感知机实际应用的一个好例子,因为它是一个相对简单的数据集,良性和恶性类别之间有明显的分隔。这意味着感知机应该能够以较高的准确度正确分类样本。此外,数据集中的特征定义明确且易于理解,这使得解释感知机模型的结果变得容易。
威斯康星州乳腺癌数据集包括以下特征:
# Attribute Domain
-- -----------------------------------------
1\. Sample code number id number
2\. Clump Thickness 1 - 10
3\. Uniformity of Cell Size 1 - 10
4\. Uniformity of Cell Shape 1 - 10
5\. Marginal Adhesion 1 - 10
6\. Single Epithelial Cell Size 1 - 10
7\. Bare Nuclei 1 - 10
8\. Bland Chromatin 1 - 10
9\. Normal Nucleoli 1 - 10
10\. Mitoses 1 - 10
11\. Class : (2 for benign, 4 for malignant)
在这个示例中,感知机将使用 Wolfram Mathematica 语言实现(可以很容易地适应任何其他语言)。
实现的步骤包括:
-
定义特征。
-
加载数据(包括清理)。
-
将数据集划分为训练集和测试集。
-
分配初始权重向量。
-
训练模型(优化权重向量)。
-
使用训练好的模型比较测试数据集。
-
计算准确率。
-
计算召回率。
-
计算混淆矩阵。
第 1 步:定义特征
features = {
"Sample code number",
"Clump Thickness",
"Uniformity of Cell Size",
"Uniformity of Cell Shape",
"Marginal Adhesion",
"Single Epithelial Cell Size" ,
"Bare Nuclei",
"Bland Chromatin",
"Normal Nucleoli",
"Mitoses",
"Class"
};
features = ToUpperCase[features];
features = StringReplace[features, " " -> "_"]
---
{"SAMPLE_CODE_NUMBER", "CLUMP_THICKNESS", "UNIFORMITY_OF_CELL_SIZE", \
"UNIFORMITY_OF_CELL_SHAPE", "MARGINAL_ADHESION", \
"SINGLE_EPITHELIAL_CELL_SIZE", "BARE_NUCLEI", "BLAND_CHROMATIN", \
"NORMAL_NUCLEOLI", "MITOSES", "CLASS"}
第 2 步:加载数据并清理
SetDirectory[
"/data/uci_breast_cancer/"];
data = Import[
"/data/uci_breast_cancer/breast-cancer-wisconsin.data"];
data = DeleteCases[data, {___, x_ /; ! NumberQ[x], ___}];
第 3 步:将数据集划分为训练集和测试集
(* OBTAIN DATA LENGTH *)
n = Length[data];
(* SET THE RATIO BETWEEN TEST AND TRAINING, TEST IS 20 PERCENT *)
testFraction = 0.2;
(* SET THE ALPHA VALUE *)
alpha = 0.9;
(* SHUFFLE THE DATA *)
randomizedData = RandomSample[data];
(* EXTRACT THE TRAINING AND TEST SETS *)
testData = Take[randomizedData, Round[testFraction*n]];
trainingData = Drop[randomizedData, Round[testFraction*n]];
(* GET THE LENGTHS OF EACH DATASET *)
lengthTestData = Length[testData]
lengthTrainingData = Length[trainingData]
---
137
546
第 4 步:分配初始权重向量
W = ConstantArray[1, Length[features[[2 ;; 10]]]]
---
{1, 1, 1, 1, 1, 1, 1, 1, 1}
第 5 步:训练模型(优化权重向量)
nonZeroSign[x_] := If[x > 0.0, 1.0, -1.0];
Do[
X = trainingData[[i, 2 ;; 10]];
Y = trainingData[[i, 11]] - 3;
EY = nonZeroSign[Dot[X, W]];
W = (W + alpha (Y - EY) X);
, {i, 1, lengthTrainingData}
];
W
---
{-2.6, 42.4, 6.4, 20.8, -78.2, 19., -26., 53.2, 6.4}
第 6 步:使用训练好的模型比较测试数据集
results = ConstantArray[0, lengthTestData];
Do[
X = testData[[i, 2 ;; 10]];
results[[i]] = nonZeroSign[Dot[X, W]];
, {i, 1, lengthTestData}
];
Y = testData[[All, 11]] - 3.0
EY = results
---
{-1., -1., 1., -1., -1., -1., 1., 1., -1., -1., -1., -1., -1., -1., \
-1., -1., -1., -1., -1., -1., 1., 1., -1., -1., -1., -1., 1., -1., \
1., -1., -1., -1., -1., -1., -1., -1., 1., -1., -1., -1., -1., -1., \
-1., -1., -1., -1., 1., -1., -1., 1., 1., -1., 1., 1., 1., -1., 1., \
-1., 1., -1., -1., -1., -1., 1., -1., 1., -1., -1., -1., 1., -1., \
-1., -1., 1., 1., 1., 1., 1., -1., 1., -1., 1., -1., -1., 1., 1., \
-1., -1., 1., 1., -1., -1., 1., 1., 1., -1., -1., -1., 1., -1., 1., \
-1., -1., 1., 1., 1., -1., -1., -1., -1., -1., -1., -1., -1., -1., \
-1., -1., 1., -1., 1., 1., -1., 1., -1., 1., -1., -1., 1., -1., -1., \
-1., -1., -1., -1., -1., -1., -1.}
{-1., -1., -1., -1., -1., -1., 1., 1., -1., -1., -1., -1., -1., -1., \
-1., -1., -1., -1., -1., 1., 1., 1., -1., -1., -1., -1., 1., -1., 1., \
-1., -1., -1., -1., -1., 1., 1., 1., -1., -1., -1., -1., 1., -1., 1., \
-1., 1., -1., 1., -1., -1., 1., -1., 1., 1., 1., -1., 1., -1., 1., \
1., -1., -1., -1., 1., 1., 1., 1., -1., -1., -1., -1., -1., -1., 1., \
1., 1., 1., -1., -1., 1., -1., 1., 1., -1., 1., 1., -1., 1., 1., 1., \
-1., -1., 1., 1., 1., -1., -1., 1., 1., -1., 1., -1., -1., 1., 1., \
-1., -1., -1., 1., -1., -1., 1., 1., 1., -1., -1., -1., 1., -1., 1., \
1., -1., 1., -1., 1., -1., -1., 1., -1., -1., -1., -1., -1., -1., 1., \
1., -1.}
第 7 步:计算准确率
testDataHit = MapThread[Equal, {EY, Y}]
testDataHitCount = Count[testDataHit, True]
EYAccuracy = testDataHitCount/Length[Y]*1.0
---
{True, True, False, True, True, True, True, True, True, True, True, \
True, True, True, True, True, True, True, True, False, True, True, \
True, True, True, True, True, True, True, True, True, True, True, \
True, False, False, True, True, True, True, True, False, True, False, \
True, False, False, False, True, False, True, True, True, True, True, \
True, True, True, True, False, True, True, True, True, False, True, \
False, True, True, False, True, True, True, True, True, True, True, \
False, True, True, True, True, False, True, True, True, True, False, \
True, True, True, True, True, True, True, True, True, False, True, \
True, True, True, True, True, True, False, True, True, False, True, \
True, False, False, False, True, True, True, True, True, True, True, \
True, True, True, True, True, True, True, True, True, True, True, \
True, True, False, False, True}
112
0.817518
第 8 步:计算召回率
recall =
Count[
Thread[Thread[Y == 1.0] && Thread[EY == 1.0]]
, True]/(
Count[
Thread[Thread[Y == 1.0] && Thread[EY == 1.0]]
, True]
+
Count[
Thread[Thread[Y == 1.0] && Thread[EY == -1.0]]
, True]
)*1.0
---
0.863636
第 9 步:混淆矩阵
beningPredictedBening =
Count[Thread[Thread[Y == -1.0] && Thread[EY == -1.0]], True]
beningPredictedMalignant =
Count[Thread[Thread[Y == -1.0] && Thread[EY == 1.0]], True]
malignantPredictedBening =
Count[Thread[Thread[Y == 1.0] && Thread[EY == -1.0]], True]
malignantPredictedMalignant =
Count[Thread[Thread[Y == 1.0] && Thread[EY == 1.0]], True]
MatrixPlot[
{
{beningPredictedBening, beningPredictedMalignant},
{malignantPredictedBening, malignantPredictedMalignant}
},
ImageSize -> 300,
ColorFunction -> "TemperatureMap",
FrameTicks -> {
{{1, "TUMOUR\nBENING"}, {2, "TUMOUR\nMALIGNANT"}},
{{1, "PREDICTED\nBENING"}, {2, "PREDICTED\nMALIGNANT"}},
{{1, ""}, {2, ""}},
{{1, ""}, {2, ""}}
},
PlotLabel -> "CONFUSSION MATRIX PERCEPTRON",
Epilog -> {
Text[beningPredictedBening, {1/2, 3/2}],
Text[beningPredictedMalignant, {3/2, 3/2}],
Text[malignantPredictedBening, {1/2, 1/2}],
Text[malignantPredictedMalignant, {3/2, 1/2}]}
]
---
74
19
6
38
图 9. 我们感知机的混淆矩阵
测量我们分类器的性能
测量一个感知机(或任何其他分类器)表现的一个方法是评估其在测试数据集上的表现。可以用来评估分类器表现的几个指标有准确率、精确度、召回率和 F1 分数。
准确率是分类器做出正确预测的百分比。它是通过将正确预测的数量除以总预测数量来计算的。然而,当类别不平衡(即一个类别比另一个类别更常见)时,准确率可能会具有误导性。
召回率是分类器正确识别的正例的百分比。它是通过将真实正例预测的数量除以总正例数量来计算的。召回率在需要最小化假阴性数量的应用中尤为重要(例如癌症检测器)。在我们的例子中,我们获得了 86%的召回率,这意味着我们的预测器遗漏了 14%的恶性肿瘤。
混淆矩阵是一个表格,显示了分类器做出的真实正例、真实负例、假正例和假负例的预测数量。它是理解分类器优缺点以及比较不同分类器性能的有用工具。
在癌症检测器中,混淆矩阵特别重要,因为它可以帮助识别分类器做出错误预测的情况。例如,如果分类器产生了大量的假阴性(即漏掉了很多癌症病例),可能需要调整分类器或收集更多的训练数据以提高其性能。另一方面,如果分类器产生了大量的假阳性(即将许多良性病例误判为癌症),可能需要调整分类器以使其在预测时更加保守。
总结
这篇文章提供了对感知机的基本数学描述,感知机是一种在 1950 年代开发的单层神经网络。它解释了感知机背后的数学原理,包括它们如何用于将数据分类到不同的类别中。
感知机被应用于一个在数据科学中广为人知的数据集,即 1995 年的威斯康星乳腺癌数据集,并演示了如何使用不同的指标来评估分类器的性能。感知机在 Mathematica 中的实现展示了如何在现代编程语言中轻松表示这些概念,实施中的不同步骤展示了如何从头设计分类器并评估其性能。
尽管感知器由于更先进的神经网络架构的出现而不再被现代机器学习实践使用,但文章显示它们仍然是理解神经网络基本原理的有价值工具。
参考文献
[1] news.cornell.edu/stories/2019/09/professors-perceptron-paved-way-ai-60-years-too-soon
[2] psycnet.apa.org/record/1959-09865-001
[3] archive.ics.uci.edu/ml/datasets/breast+cancer+wisconsin+(diagnostic)
| archive-beta.ics.uci.edu/dataset/15/breast+cancer+wisconsin+original
(CC BY 4.0 许可,见致谢)。
致谢
使用的数据集由 UCI 机器学习库提供。数据集由以下人员创建:
1. Dr. William H. Wolberg,普通外科系。
威斯康星大学,临床科学中心
麦迪逊,WI 53792
2. W. Nick Street,计算机科学系。
威斯康星大学,1210 West Dayton St.,麦迪逊,WI 53706
3. Olvi L. Mangasarian,计算机科学系。
威斯康星大学,1210 West Dayton St.,麦迪逊,WI 53706
捐赠者:Nick Street。
UCI 机器学习库 archive.ics.uci.edu/ml
。加州欧文:加州大学信息与计算机科学学院。 (archive.ics.uci.edu/ml/about.html
/ archive.ics.uci.edu/ml/citation_policy.html
)。
每位数据领导者成功所需的 3 项核心技术技能
赋能数据领导者掌握关键技术技能,以推动业务洞察
·发布于 Towards Data Science ·4 min read·2023 年 6 月 19 日
--
图片由 Nick Fewings 提供,来源于 Unsplash
根据Gartner的调查,CDO 的平均任期仅为 2.4 年。这一令人震惊的发现突显了数据领导者面临的挑战,并强调了为他们提供实现持久成功所需的核心技术技能的重要性。
尽管作为数据领导者,软技能的关注度非常高,但不幸的是,在早期阶段,技术技能的缺乏有时会导致知识不足,从而导致任期较短。
今天,让我们深入探讨三大关键技术知识领域,赋能数据领导者应对数据分析的复杂性,推动创新,并最终在其组织中产生持久影响。
1. 解释和传达架构的能力
架构知识对数据领导者的成功至关重要。
你需要处理的首要任务之一是确保为组织提供数据的平台稳健可靠。然而,缺乏架构知识的领导者往往难以理解和表达其重要性。
数据架构是一项技术技能,应与沟通这一软技能结合,以便将信息传达给技术和非技术利益相关者。这还包括分析和解释各种数据架构组件,如数据库、数据仓库、数据湖和数据管道。
有效的架构沟通促进了协作和决策。
你应该能够提出/回答的示例问题:
-
我们的客户-facing 系统是什么?
-
我们多久从中提取一次信息?
-
组织中的所有数据存储在哪里?
-
通常的数据流是什么样的?
-
捕获数据后我们能多快分析它?
-
哪些数据支持我们的 AI / ML 模型?
-
我们目前如何管理这些数据/应该如何管理这些数据?
-
数据质量如何?
-
我们的约定数据定义是什么?
2. 从用例定义数据价值
如果数据没有提供价值,它就是负担,而不是资产。
作为数据领导者,你应该能够透过繁杂和噪音,准确找出最终解决业务问题的数据。数据领导者还必须具备评估组织战略目标和需求的能力,并将其转化为可操作的数据驱动用例。
这一技能需要对业务领域和现有数据资产有深入了解。
你应该能够提出/回答的示例问题:
-
我们的关键数据资产是什么?
-
数据资产如何转化为产品?
-
数据质量不足如何影响最终业务用例?
-
我们用这些数据解决了哪些业务用例?
-
如何利用这些数据生成收入和降低风险?
-
CFO / CRO / CxO 的优先事项是什么,我的数据功能如何支持这些优先事项?
3. 理解数据资产中的复杂性和冗余
简化数据资产,提升效率。
除了收入生成机会,还应关注成本效率。成功的组织很快就会超越其 IT 和数据资产。数据领导者必须了解他们的数据资产以及可以改进或简化的领域。发现冗余可以帮助你简化操作、优化存储和提高数据质量。
这一技能带来简化、高效且现代的数据资产,推动有价值的洞察和业务成果。
你应该能够提出/回答的示例问题:
-
有多少数据源或存储区域是冗余的?
-
组织中有哪些数据孤岛,这些数据孤岛的影响是什么?
-
如果某些操作数据存储被停用,成本效率会如何变化?
-
如何减少复杂性以简化数据治理和质量?
-
哪些业务流程依赖于数据资产中的复杂部分?
结论
软技能仍然至关重要;你必须能够与同事谈判,与不同受众沟通,简化复杂主题等。然而,你还必须具备技术技能,以赢得团队和领导的信任。
实施数据质量(DQ)是工作中最困难的部分之一。如果你想向你的领导团队推销 DQ 并实施其核心方面,请查看我的免费终极数据质量手册。通过领取你的副本,你还将成为我们教育社区的一部分,通过我们的邮件列表获得有价值的见解和更新。
介绍《终极数据质量手册:完善数据的最佳实践》。在今天这个数据驱动的世界里……
hanzalaqureshi.gumroad.com](https://hanzalaqureshi.gumroad.com/l/cfijx?layout=profile&source=post_page-----a1800c644469--------------------------------)
如果你还没有订阅 Medium,可以考虑使用我的推荐链接订阅。它比 Netflix 便宜,并且显然更值得你的时间。 如果你使用我的链接,我会获得少量佣金,而你将获得 Medium 上的无限故事,双赢。
我永久切换从 Pandas 到 Polars 的 3 个理由
我来是为了速度,但我留在这里是为了语法
·发表于 Towards Data Science ·阅读时间 10 分钟·2023 年 3 月 28 日
--
照片由 Hans-Jurgen Mager 供稿,刊登在 Unsplash
在撰写这篇文章时,我已经在数据科学领域工作了六年。而在这六年中,[Pandas](https://pandas.pydata.org/)
一直是我所有工作的基础:探索性数据分析、影响分析、数据验证、模型实验等。我的职业生涯是建立在Pandas
之上的!
不用说,我曾经在Pandas
中有过严重的锁定。
也就是说,直到我发现了[Polars](https://github.com/pola-rs/polars)
,这个新的“极其快速的 DataFrame 库”。
在这篇文章中,我将解释:
-
Polars
是什么,以及是什么让它如此快速; -
我永久切换从
Pandas
到Polars
的 3 个理由;-
.list
命名空间; -
.scan_parquet()
和.sink_parquet()
; -
面向数据的编程。
-
介绍 Polars:你可能从未听说过的最快 Python 数据框库。
也许你听说过Polars
,也许你没有!无论如何,它正在慢慢改变 Python 的数据处理领域,就从这里的 Towards Data Science 开始:
-
Leonie Monigatti 最近写了一篇 Pandas 与 Polars 的全面时间比较。
-
Wei-Meng Lee 已于去年夏天发布了 入门指南。
-
Carl M. Kadie 几个月前在 一个关于
[Pandas](https://medium.com/towards-data-science/understand-polars-lack-of-indexes-526ea75e413)
和[Polars](https://medium.com/towards-data-science/understand-polars-lack-of-indexes-526ea75e413)
的最大表面差异之一 中写道[Polars](https://medium.com/towards-data-science/understand-polars-lack-of-indexes-526ea75e413)
没有索引。
那么,是什么让 Polars
如此快速?来自 [Polars](https://pola-rs.github.io/polars-book/user-guide/#introduction)
用户指南:
Polars
完全用[Rust](https://www.rust-lang.org/)
编写(没有运行时开销!),并使用[Arrow](https://arrow.apache.org/)
– 作为其基础的 原生 arrow2[Rust](https://github.com/jorgecarleitao/arrow2)
实现……
Polars
是用 Rust 编写的,这使它具备了 C/C++ 的性能,并且能够完全控制查询引擎中的性能关键部分………与像 dask 这样的工具不同,dask 试图并行化现有的单线程库如 NumPy 和 Pandas,而 Polars 从零开始编写,专为 DataFrame 上的查询并行化设计。
就是这样。Polars
不仅仅是一个用于缓解 Pandas
单线程特性的框架,像 [dask](https://docs.dask.org/en/stable/)
或 [modin](https://modin.readthedocs.io/en/latest/#)
;相反,它是对 Python 数据框的全面改造,包含了作为基础的高度优化的 Apache Arrow 列存储内存格式,还有其自身的查询优化引擎。而速度方面的结果令人惊叹(根据 h2oai 的数据基准测试):
图片来源于 h2oai 的数据基准工具。
对于一个 5GB 的数据框进行 groupby 操作时,Polars
比 Pandas
快 6 倍以上!
仅仅是这速度就足以引起任何人的兴趣。但正如你将在本文其余部分看到的,速度吸引了我,但真正让我爱上的是其语法。
我为什么永久切换到 Polars 的 3 个理由
1. .list
命名空间
想象一下在Pandas
中的以下场景:你有一个包含家庭及其信息的数据集,其中包括家庭成员的所有成员列表:
import pandas as pd
df = pd.DataFrame({
"last_name": ["Johnson", "Jackson", "Smithson"],
"members": [["John", "Ron", "Con"], ["Jack", "Rack"], ["Smith", "Pith", "With", "Lith"]],
"city_of_residence": ["Boston", "New York City", "Dallas"]
})
print(df)
>>>> last_name members city_of_residence
0 Johnson [John, Ron, Con] Boston
1 Jackson [Jack, Rack] New York City
2 Smithson [Smith, Pith, With, Lith] Dallas
对于你的分析,你想从members
列表的第一个元素创建一个新列。你该怎么做?搜索Pandas
API 会让你迷失,但简单的 stack overflow 搜索会告诉你答案!
提取Pandas
列中列表元素的主要方法是使用.str
命名空间(stackoverflow ref1, stackoverflow ref2),像这样:
df["family_leader"] = df["members"].str[0]
print(df)
>>>> last_name members city_of_residence family_leader
0 Johnson [John, Ron, Con] Boston John
1 Jackson [Jack, Rack] New York City Jack
2 Smithson [Smith, Pith, With, Lith] Dallas Smith
如果你像我一样,你可能会想,“为什么我必须使用.str
命名空间来处理list
数据类型?”。
不幸的是,Pandas
的.str
命名空间不能完成所有可能需要的list
操作;例如,有些操作需要代价高昂的.apply
。而在Polars
中,这不是问题。通过符合 Apache Arrow 的列式数据格式,Polars
具有所有标准数据类型,并有适当的命名空间来处理所有这些类型——包括list
:
import polars as pl
df = pl.DataFrame({
"last_name": ["Johnson", "Jackson", "Smithson"],
"members": [["John", "Ron", "Con"], ["Jack", "Rack"], ["Smith", "Pith", "With", "Lith"]],
"city_of_residence": ["Boston", "New York City", "Dallas"]
})
df = df.with_columns([
pl.col("members").list.get(0).alias("family_leader")])
print(df)
>>>> ┌───────────┬─────────────────────────────┬───────────────────┬───────────────┐
│ last_name ┆ members ┆ city_of_residence ┆ family_leader │
│ --- ┆ --- ┆ --- ┆ --- │
│ str ┆ list[str] ┆ str ┆ str │
╞═══════════╪═════════════════════════════╪═══════════════════╪═══════════════╡
│ Johnson ┆ ["John", "Ron", "Con"] ┆ Boston ┆ John │
│ Jackson ┆ ["Jack", "Rack"] ┆ New York City ┆ Jack │
│ Smithson ┆ ["Smith", "Pith", … "Lith"] ┆ Dallas ┆ Smith │
└───────────┴─────────────────────────────┴───────────────────┴───────────────┘
没错:Polars
对数据类型非常明确,甚至在每次打印数据框时都会告诉你每一列的数据类型!
但这还不是全部。Pandas
的 API 不仅需要使用一个数据类型的命名空间来处理另一个数据类型,而且 API 变得非常臃肿,往往有许多种方法来做同一件事。这可能会令人困惑,尤其是对新手而言。考虑以下代码片段:
import pandas as pd
df = pd.DataFrame({
"a": [1, 1, 1],
"b": [4, 5, 6]
})
column_name_indexer = ["a"]
boolean_mask_indexer = df["b"]==5
slice_indexer = slice(1, 3)
for o in [column_name_indexer, boolean_mask_indexer, slice_indexer]:
print(df[o])
在这个代码片段中,相同的Pandas
语法df[...]
可以执行三种不同的操作:检索数据框的一列,在数据框上执行基于行的布尔掩码,以及按索引检索数据框的切片。
另一个令人困扰的例子是,要处理dict
列时,使用Pandas
通常需要执行一个代价高昂的apply()
函数;而Polars
则有一个struct
数据类型,可以直接处理dict
列!
在Pandas
中,你不能完成所有你想做的事情,对于可以做的事情,有时有多种方式来完成。与此相比,Polars
可以做所有事情,数据类型明确,通常只有一种方法来做同一件事。
2. .scan_parquet()
和 .sink_parquet()
Polars
最棒的地方之一是它提供了两个 API:一个急切的 API 和一个延迟的 API。
急切的 API 会像Pandas
一样在内存中运行所有命令。
然而,延迟 API 只有在明确要求响应时(例如,通过.collect()
语句)才会执行所有操作,有点像dask
。而且,一旦被要求响应,Polars
会依赖其查询优化引擎,以最快的时间提供结果。
考虑以下代码片段,比较Polars
的急切DataFrame
与其延迟对应物LazyFrame
的语法:
import polars as pl
eager_df = pl.DataFrame({
"a": [1, 2, 3],
"b": [4, 5, 6]
})
lazy_df = pl.LazyFrame({
"a": [1, 2, 3],
"b": [4, 5, 6]
})
语法非常相似!实际上,急切 API 和延迟 API 之间唯一的主要区别在于数据框的创建、读取和写入,这使得在两者之间切换变得非常容易:
作者表
这就引出了.scan_parquet()
和.sink_parquet()
。
通过使用[.scan_parquet()](https://pola-rs.github.io/polars-book/user-guide/lazy-api/streaming.html)
作为数据输入函数,[LazyFrame](https://pola-rs.github.io/polars-book/user-guide/lazy-api/streaming.html)
作为数据框,和[.sink_parquet()](https://pola-rs.github.io/polars-book/user-guide/lazy-api/streaming.html)
作为数据输出函数,你可以处理超出内存的数据集! 这真的很酷,尤其是当你把它与Pandas
的创始人 Wes McKinney 在 2017 年发布的“Apache Arrow 和‘我讨厌 Pandas 的 10 件事’”中关于Pandas
内存占用的说法进行比较时:
“我的 Pandas 使用规则是,你的 RAM 应该是数据集大小的 5 到 10 倍。”
3. 数据导向编程
Pandas
将数据框视为对象,支持面向对象编程;但Polars
将数据框视为数据表,支持数据导向编程。
让我解释一下。
使用数据框时,我们大多数时候要做的是运行查询或转换;我们要添加列,按两个变量进行透视,聚合,分组,等等。即使当我们想将数据集分为训练集和测试集以训练和评估机器学习模型时,这些本质上也是类似 SQL 的查询表达式。
这是真的——使用Pandas
,你可以对数据进行大多数你想要的转换、操作和查询。然而,让人沮丧的是,有些转换和查询只能在多个表达式或查询中完成。与 SQL 或 Spark 等其他查询和数据处理语言不同,Pandas
中的许多查询需要多个连续的、独立的赋值表达式,这可能使事情变得复杂。考虑以下代码片段,我们创建了一个包含人员及其年龄的数据框,并且我们想要查看每个十年中有多少人:
import pandas as pd
df = (
pd.DataFrame({
"name": ["George", "Polly", "Golly", "Dolly"],
"age": [3, 4, 13, 44]
})
)
df["decade"] = (df["age"] / 10).astype(int) * 10
decade_counts = (
df
.groupby("decade")
["name"]
.agg("count")
)
print(decade_counts)
>>>> decade
0 2
10 1
40 1
这无可避免——我们必须在三个赋值表达式中完成查询。为了将其缩减为两个表达式,我们本可以使用不常见的[.assign()](https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.assign.html)
操作符来代替df["decade"] = ...
操作,但仅此而已!这可能在这里看起来不是大问题,但当你发现自己需要七、八、九个连续的赋值表达式来完成任务时,情况可能会变得有点难以阅读和维护。
在Polars
中,这个查询可以被干净地写成一个表达式:
import polars as pl
decade_counts = (
pl.DataFrame({
"name": ["George", "Polly", "Golly", "Dolly"],
"age": [3, 4, 13, 44]
})
.with_columns([
((pl.col("age") / 10).cast(pl.Int32) * 10).alias("decade")
])
.groupby("decade")
.agg(
pl.col("name").count().alias("count")
)
)
print(decade_counts)
>>>> ┌────────┬───────┐
│ decade ┆ count │
│ --- ┆ --- │
│ i32 ┆ u32 │
╞════════╪═══════╡
│ 0 ┆ 2 │
│ 10 ┆ 1 │
│ 40 ┆ 1 │
└────────┴───────┘
真的很顺畅。
你可能会读到这些内容后,心里想着“为什么我还要在一个表达式中做所有事情呢?”。确实,也许你不需要。毕竟,许多数据管道使用中间查询,将中间结果保存到表中,并查询这些中间表以获得最终结果,甚至用于监控数据质量。
但像 SQL、Spark 或其他非Pandas
的数据处理语言一样,Polars
给予你 100%的灵活性来在你希望的地方拆分查询,以最大化可读性,而Pandas
则强制你根据其 API 的限制拆分查询。这不仅对代码可读性是一个巨大的好处,而且对开发的便利性也是如此!
更进一步,作为额外的好处,如果你使用Polars
的惰性 API,你可以在任何地方将查询拆分为任意多的部分,而整个过程最终会在后台被优化成一个查询。
结论
我在这篇文章中讨论的只是Polars
相对于Pandas
的优越性的一个缩影;Polars
中仍有许多功能继承自 SQL、Spark 和其他数据处理语言(例如[pipe()](https://pola-rs.github.io/polars/py-polars/html/reference/dataframe/api/polars.DataFrame.pipe.html#polars.DataFrame.pipe)
、[when()](https://pola-rs.github.io/polars/py-polars/html/reference/expressions/api/polars.when.html#polars.when)
和[filter()](https://pola-rs.github.io/polars/py-polars/html/reference/dataframe/api/polars.DataFrame.filter.html#polars.DataFrame.filter)
,仅举几例)。
尽管现在Polars
是我在 Python 中进行数据处理和分析的首选库,我仍然使用Pandas
来处理一些特定的用例,比如为报告和演示文稿中的数据框进行样式设置或与电子表格进行沟通。也就是说,我完全预计Polars
会随着时间的推移逐步取代Pandas
。
接下来是什么?
当
开始使用新工具是困难的,尤其是当它是一个新的 dataframe 库时,这对我们作为数据科学家来说至关重要!我通过参加Liam Brannigan的 Udemy 课程“使用 Polars 进行数据分析”,并且强烈推荐这个课程——它涵盖了Polars
的所有基础知识,并且让我过渡得非常顺利(我推荐这个课程并没有获得任何推荐奖金;我只是觉得它非常好!)。
作为另一种选择,尽管 Polars 通常被学习为 Pandas 的直接替代品,但由于两者之间语法差异较大,从 Pandas 过渡到 Polars 可能会有些奇怪。因为 Polars 更类似于 SQL 而不是 Pandas,所以通过将 Polars 与 SQL 进行比较来学习 Polars 可能是一条更简单的路径,这正是我在后面的文章中所做的,“Polars 查询的结构:Polars 与 SQL 的语法比较”
## Polars 查询的结构:Polars 与 SQL 的语法比较
从 Pandas 过渡到 Polars 的简单方法——先在 SQL 上停留一下。
towardsdatascience.com
致谢
特别感谢Liam Brannigan的Polars
课程,没有它我不确定自己是否能顺利过渡到Polars
。当然,也要感谢Richie Vink,Polars
的创始人!你不仅创建了一个很棒的库,还迅速回应了我在 LinkedIn 和 Github 上关于Polars
的问题和评论——你不仅创造了一个了不起的工具,还建立了一个友好的社区。还有你,读者——感谢你的阅读;祝你数据处理愉快 😃
联系方式
喜欢你所读到的内容吗?随时与我联系:
社交媒体:LinkedIn | Twitter | Instagram | YouTube
支持:请给我买杯咖啡!
查看我网站上的博客文章 我的网站!
今年提升数据技能的 4 种小而强大的方法
通过掌握这四项技能提升你的数据能力
·发布在 Towards Data Science ·4 分钟阅读·2023 年 1 月 23 日
--
图片来源:Miguel A Amutio 通过 Unsplash
一位同事最近问如何在今年提升他们的数据技能。
数据技能可以有多种形式。从数据工程、分析到数据科学和可视化。然而,它们都有一个共同的目标:驱动业务价值。这意味着,与技能相关的问题的答案更多是价值驱动的,而不是技术性的。
所以,让我们探讨一下提升数据技能的四种方法。
1. 找到数据问题的根本原因
数据问题总是存在的。
无论是质量、基础设施还是性能。你的组织将面临许多数据问题。
拥有分析技能以准确定位数据生命周期中的问题是至关重要的。最近,一个组织的执行指标报告被延迟了。IT 团队因数据交付延迟而受到主要影响,而延迟是由于核心逻辑更改导致的。事情往往不像它们看起来的那样。因此,具备分析技能以提出正确的问题是至关重要的。
快速高效地找到数据问题的根本原因是一项重要技能。分析你组织的最新十大数据问题可以帮助你提升这项技能。一旦问题得到解决,人们通常会继续前进;如果你花时间去理解它,你可以在下一个问题上更有效。
投资时间学习分析问题、找到根本原因并提供修复步骤。
2. 用简单语言解释数据概念
如果你不能用简单的术语解释某事,你就不够理解它 — 理查德·费曼。
数据是一个复杂的话题,有许多聪明的人在解决挑战性问题。然而,只有少数人能用没有术语的方式向更广泛的业务解释概念。
成为一个好的讲故事者在数据行业中是一个被低估的技能。在我早期的日子里,我记得总是难以简明扼要地提供工作更新。实际上,很多人并不在乎细节;他们想要的是“TL;DR”版本。学会简化话题,使用类比,避免术语。如果信息没有传达,调整并重试。
好的讲故事能帮助你传达信息,让听众保持参与感并建立可信度。将你的工作提炼成简单的概念/类比并练习讲解。这就是提升你讲故事技能的方法。
简单地解释,甚至一个五岁的孩子也能理解。
3. 理解数据背后的业务目标
如果数据无法解决业务问题,那它就是无用的。
作为数据知识工作者,我们花费大量时间调整优先级。然而,关于这些活动如何推动整体进展的更大图景常常被忽视。
将你的日常工作与最终业务目标关联起来是一个被低估的技能。我们曾有一个分析师团队花了四周时间找到数据质量问题的根本原因,但最终发现解决方案对业务没有任何影响。虽然解决了某些无聊的技术挑战,但并没有改善客户体验或减轻任何业务风险。如果它不帮助业务目标,那为什么它重要?
牢记业务目标以避免上述情形是至关重要的。总是问“为什么”,直到你满意这确实是核心问题。如果不满意,再问一次“为什么”。
连接数据与业务之间的点;只有少数人在做这件事。
4. 战略思考
我们都花费太多时间进行应急处理。
应急处理让你成为一个出色的战术思考者。然而,你会失去战略目标和未来规划的思考过程。
解决数据问题的方法可能是快速但战术性的,或者是长期但战略性的。十有八九,我们会选择战术性的解决方案来止血;我自己也这么做过。但几个月后,未采取的战略解决方案会反噬回来。考虑你提议的结果的长期影响。否则,你只是扑灭了一个火焰,却点燃了未来更大的火焰。
无论你在组织中的资历如何,战略性思考都能给你带来优势。质疑你即将实施、展示、修复、书写等的内容是否会对业务和你的信誉产生长期影响。如果是的话,寻找替代方案。
成为一个战略思考者;这是一个稀有的技能。
结论
是的,你需要了解 SQL 和 Python 以及选择的可视化工具,但这些只是手段或支持者。技术领域不断发展,但这些软技能将始终适用。优先提升它们,以提升你的技能。
提升数据质量技能是投资回报最大之一,查看我终极数据质量手册的免费副本,帮助你掌握这一技能并加入我的电子邮件订阅列表。
介绍《终极数据质量手册:完善数据的最佳实践》。在当今数据驱动的世界里……
如果你还没有订阅 Medium,可以考虑使用我的推荐链接订阅。它比 Netflix 更便宜,而且显然是更好的时间利用。 如果你使用我的链接,我会获得少量佣金,你则可以无限制访问 Medium 上的故事,双赢。
数据故事讲述中的 4D:将科学变为艺术
原文:
towardsdatascience.com/the-4ds-in-data-storytelling-making-art-out-of-science-c4998ed7875e
图片来源:Melanie Deziel 在 Unsplash
是的,这远远超出了数据可视化的范围
·发表于 Towards Data Science ·14 分钟阅读·2023 年 10 月 3 日
--
数据无处不在。任何具备一定训练的人,如今借助 AI 的帮助,都可以从数据中生成一些科学洞察,并构建华丽的数据可视化。然而,解释和传达数字和图表背后的含义是一门艺术。当 ChatGPT 和生成型 AI 登上前台时,许多关于被 AI 取代的担忧浮现。通过明确的指令,AI 可以帮助我们生成代码、可视化,甚至构建包含有用洞察的高效模型,但它们在基于这些洞察编写引人入胜的可信且令人难忘的故事方面却力不从心。它们可以做科学,但艺术是人类独有的技能,至少目前是这样。
图片来源:Brett Jordan 在 Unsplash
根据受众的不同,这些数据故事在建立信任、促进合作或影响业务决策方面至关重要。没有故事讲述的数据科学家的工作仅仅是数字占卜。本文将分享一个4D 框架,帮助数据科学家破解数据故事讲述过程,提高数据洞察的效率和影响力,并在最后附上实际建议的额外部分。
定义
讲故事的第一步是定义故事。什么才是故事?虽然虚构作家可能会给出更全面的答案,但本质上,故事是传达一系列事件的叙述,包括背景设置、角色和情节。为了使故事有趣,它必须吸引人、引人入胜、娱乐性强或具有信息性。数据讲故事的过程始于定义一个故事,通过建立相关的背景、引人注目的角色和迷人的情节来保持观众的兴趣。
背景
当你从数据中发现有趣的结果时,你需要在传递发现之前为接收者设置一个相关的背景。故事的背景为后续的沟通建立了上下文。我们首先需要了解这次沟通的媒介是什么。是演示文稿还是书面报告?是深度技术会议还是高层结果评审?这将指导你的故事朝不同的方向发展。
然后,我们需要定义故事中的什么和为什么。背景是什么?所有参与沟通的人需要达成一致。问题是什么?我们为什么要进行这次沟通?
此外,这次沟通的行动点是什么?假设每个人都同意你的故事,下一步是什么?
在展开你的故事之前设置背景是至关重要的。这有助于你更有条理和高效地准备整个沟通,并帮助观众与你的故事产生共鸣,使他们与你处于同一页面上。
角色
照片由 Kenny Eliason 提供,来源于 Unsplash
角色是故事的灵魂。一个好的数据故事应该把你和观众都融入故事中。这里有两个方面:观众是谁, 和 他们与你的关系是什么?不幸的是,没有一种灵丹妙药能让所有类型的观众与你和你的故事产生共鸣。不同的观众有不同的痛点,这使得有必要相应地调整你的信息。在准备故事时,问自己这些问题:
-
观众是谁?他们在技术上强大还是以业务为驱动?他们的日程是否很忙?他们只关心总体概述,还是关注细节?他们如何从这次沟通中受益?
-
他们与你的关系是什么?他们是需要你建立信任和信誉的新对象吗?还是你们已经建立了关系?你需要他们的合作,还是在给他们结果?你需要他们做出决定或采取行动吗?
这些问题将帮助你导航准备工作。请注意,有时你可能对观众做出了错误的假设。例如,你认为他们不关心细节,但实际上,他们对你的详细思路非常感兴趣。因此,与大家进行事前沟通以对齐期望是至关重要的。或者,你也可以准备备用幻灯片或证据,以防后续问题进入次轨道。
情节
情节是故事的脊梁。考虑传统的三幕剧结构,这是一个广泛使用的框架,用于组织和呈现文学、戏剧和电影中的叙述。这种结构将故事分为三个不同的幕,每一幕都有其独特的目的和事件顺序:
image by author
我们可以在讲述数据故事时使用相同的框架。在我们数据故事的第一幕中,我们可以简要介绍背景和环境,以确保每个人都了解背景。然后,在第二幕中,我们深入探讨数据和发现,介绍你的观众关心的挑战或试图解决的问题。在这里,你会转向沟通的重点。可能是识别出一个令人惊讶的趋势或一个重要的发现。立刻,你展示一个突破,解决这些挑战,也许发现一个隐藏的模式,重新塑造大家的视角。第三幕是关于解决方案的,我们提出解决方案和行动点。最后,花一点时间考虑我们数据故事的广泛影响,让观众带走难忘的收获或所需的行动。沟通的逻辑和流程将定义观众如何接收和反应这些信息。一个难忘的故事情节起伏跌宕,观众对这些故事产生共鸣,因为它们帮助他们解决他们面临的痛点。
展示
现在我们有了结构框架,我们需要搞清楚如何在骨架上构建内容。数据故事包含叙述和视觉支持,如幻灯片或图表。在本节中,我将主要关注如何选择与数据故事匹配的数据可视化,以及使用哪些工具来帮助更好地传达信息。
照片由Daria Nepriakhina 🇺🇦拍摄,来自Unsplash
不同的情节
在可视化类型中有许多选择来表达观点。根据情况,我们可以选择折线图、柱状图、散点图、饼图、表格或只是文本。以下是一些实际中的常见用例:
-
折线图: 折线图有助于展示一个或多个变量随时间的变化,使其成为可视化数据趋势和模式的绝佳选择。折线图可以用来显示一个城市在一年的温度变化。这使观众可以轻松识别任何季节性模式或温度趋势。
-
饼图: 饼图是展示数据集或变量组成部分或比例的有用工具。当你想强调不同组件的相对大小时,它们特别有效。例如,饼图常用于描绘家庭每月支出的分布,包括租金、公用事业费、食品杂货和娱乐等项目。这提供了资金分配的清晰视觉表示。
-
条形图: 条形图非常适合用于比较类别之间的值。饼图也能显示比较,但我们可以在条形图中添加时间范围,将比较扩展到时间维度。例如,我们可以使用条形图显示过去五年不同产品的销售情况。此外,条形图也广泛用于显示数据分布的直方图。
-
散点图: 散点图有助于分析两个连续变量之间的关系和相关性。它可以帮助识别数据中的模式、集群或异常值。例如,如果你想确定某一产品的销售额和价格变化是否存在相关性,散点图可以可视化这两个变量之间的关系。
-
表格: 表格是呈现详细数据的有价值工具。当观众需要获取特定数据点时,它们尤其有效。需要注意的是,表格通常显示一个数据维度,要么是横截面数据,要么是时间序列数据。横截面表格展示跨主题的值比较,而时间序列表格展示一个主题在时间上的值比较。包含主题和时间范围的面板表格可能过于详细,不适合口头呈现,但在书面形式中常用作有力的支持证据或进行更深入的分析。虽然表格可以用于显示时间序列比较,但折线图通常更有效。因此,我们更多地使用表格来处理横截面数据。
-
文本: 将文本视为可视化并不常见,但它可以是传达信息的最有效方式之一。它可以是一个突出关键统计数据的句子。像下面这样的文本信息可以立即吸引观众的注意。
image by author
可以在图表中添加文本以提供背景、注释或解释。这有助于更好地呈现信息。
不同的可视化工具:
我们可以使用许多可视化工具来生成理想的图表。我们有专业工具如 Tableau 和 PowerBI,流行的 Python 包如 Plotly,以及用户友好的 Excel。无论使用哪种工具,主要目的是正确有效地传达信息,而不是展示你能写出多么复杂的代码或生成多么华丽的图表。如果你对 Excel 更为熟悉,那就不要浪费时间去弄清楚 Python 的语法。
另一个要考虑的因素是你是否需要共享可视化以及如何共享。是静态图表或截图,还是需要与接收者进行互动?此外,这个图表是一次性展示,还是需要在数据更新时定期重新生成?如果需要重新生成图表,设置模板或编写模块化代码以节省未来的时间是有帮助的。
清理冗余
少即是多,除非你在撰写研究论文或纪录片,需要为几乎每一句话提供大量的证据。一旦你有了故事线和支持的可视化效果,下一步就是考虑如何去除不必要的细节,以便观众能更容易地跟随你的故事而不会分心。
图片来源:Prateek Katyal 在 Unsplash
探索性 vs 解释性
虽然可能需要几周的时间来发现洞察或创建模型,但展示整个思考过程是不必要的,除非观众特别要求。专注于观众最关心的内容。以商业为导向的观众关心你的发现如何有利于他们的业务 KPI,而技术观众关心你为什么以这种方式处理问题,以及你的模型表现如何。根据时间和格式,根据观众的需求调整故事的长度。
去除多余内容
对于特定的幻灯片或图表,考虑去除冗余内容。思考主要信息是什么,哪些是多余的细节。当你展示的信息较少时,观众更容易把握主要信息。爱德华·塔夫提(Dr. Edward Tufte)提出了一个公式来量化图表中的冗余:
图片由作者提供
理想的数据墨水比率应为 1,即每一滴墨水都应支持你所讲述的故事。否则,你需要去除所谓的 图表垃圾。常见的图表垃圾包括:
-
网格线,你可以标注所选的数据点;
-
冗余的坐标轴,尤其是当数据点已被标记时;
-
图表区域中的不必要的标注或标记;
-
分散注意力的图像或图标。
这是简化前后的示例:
作者提供的图片
请注意,简化并不等于去除重复。多次强调主要信息以加深观众的印象是必要的。此外,记住你的观众可能不如你对数据熟悉,因此你可能需要通过重复来更多地构建和强调背景。
直接
尽管观众的注意力最终不在你的控制之下,但有一些好的实践可以帮助抓住观众的注意力,从而帮助你正确高效地传达信息。这样,你更有可能影响商业决策,获得未来的支持,建立信任和信誉等。一般来说,你可以消除干扰、突出主要信息和调整和结构化交付来帮助引导观众的注意力。**我们在上一部分讨论了通过简化来消除干扰。让我们在这一部分关注后两者。
突出主要信息
在构建数据故事时,我们有主要信息和支持证据或比较,以帮助更好地表达观点。在这种情况下,我们需要突出主要信息,以便观众不会在信息的海洋中迷失。当确定主要信息时,从你发现的内容开始,然后问自己哪些发现会引起观众的兴趣,你希望观众如何使用这些信息。通过消除观众可能已经同意的点来关注影响。之后,我们可以通过预注意属性来引导观众的注意力。这些属性包括:
-
大小。增加主要信息或关键统计数据的大小;
-
颜色。注意不要在一页或一张幻灯片上使用太多颜色。保持相同的颜色调色板。有时,调整透明度也会有效。我们通常不需要彩虹;
-
通过使用不同的字体、加粗、斜体或下划线来突出显示;
-
添加特殊符号,如箭头或文本注释以帮助解释;
-
如果你使用 PowerPoint 进行演示,请使用动画逐步展示你的故事;
-
根据情境,我们可以添加互动图表,以根据观众的反馈和评论显示不同的信息。
调整和结构化交付
你已经弄清楚了要向观众展示的内容。现在是时候找出一个结构,以逻辑顺序传达信息,让观众容易接受。我们需要考虑水平流程和垂直流程。
Maksym Tymchyk 🇺🇦 在 Unsplash 上的照片
我们在“定义”部分提到的三幕结构帮助我们将故事的逻辑横向流动逐步展示。常见的叙事流程有:
-
按时间顺序: 按事件的时间顺序展示,并连接其影响;
-
从大到小: 首先展示高层次的大图,然后再将其拆解为更多的细节,并提供更多的支持证据;
-
概念性: 按逻辑顺序展示,比如 IF -> THEN -> So;
-
按组别: 首先展示整体情况,然后再深入到每个感兴趣的组别;
通常,无论你最终遵循哪种横向流程,我们都应该始终保持“总结 — > 细节 — > 总结”的格式。开始时向观众提供一个大纲,在过程中与观众的痛点相连接,并以行动点结束。采取行动是数据讲故事的影响所在。
保持故事中的合理纵向流动也至关重要。幻灯片中的纵向流动是在标题和数据视觉准确突出相同观点的不同细节层次时实现的。在商业演示中,确保每张幻灯片传达清晰的信息,数据和视觉效果支持幻灯片标题,并清晰展示见解。
附加部分
现在你已经学习了一个实用的框架来构建故事,那么在沟通过程中有哪些良好的做法可以帮助你更好地传达信息呢?以下是一些有用的建议:
保持自信
你可能没有观众那么有经验或技术高超,但记住,没有人比你自己更了解你的工作。你是你数据故事唯一的专家。
征求反馈
在沟通之前、期间和之后征求反馈。你可以事先了解观众的期望。然后,和其他人练习,询问他们是否理解了你希望传达的信息。在演讲过程中,设置一定的中断点,询问观众是否有任何问题,是否都跟上了。演讲结束后,跟进行动点、演讲中未回答的问题,并询问观众对未来沟通的反馈。
观察和练习
观察他人在不同场景中如何传达信息。观察经验丰富的同事有效地展示技术发现,经理如何说服利益相关者延长项目时间,或产品负责人如何在发布日挑选产品特性。这不仅限于你的工作环境,也可以来自日常生活,比如当你被销售人员接触,或者观看最新的苹果开发者大会(WWDC)等。
最重要的是,不要忘记总结学习内容并付诸实践。与自己或他人进行彩排,了解整个故事的呈现时间,以及你是否通过这个故事讲解得清楚。
练习向非技术观众进行技术演讲
数据科学家向非技术观众展示黑箱模型预测是很常见的。在讲述故事之前,弄清楚他们需要什么。平衡技术证据与令人兴奋的结果。为了向非技术观众解释必要的技术术语,我发现保持高层次的讲解、给出相关的例子并始终与结果联系起来是有益的。不要忘记在继续之前检查你的观众是否理解了内容。
注意时间
尊重观众的时间非常重要,要将你的数据故事限制在约定的时间内。为了准确知道需要包含多少信息,你应该提前进行排练。然而,演讲过程中总会发生意外,比如话题偏离或要求更多细节。在这种情况下,除了始终在结尾留出缓冲时间外,你还需要在观众要求过多细节或话题偏离时保持警觉。你可以说:“为了节省时间,我们可以离线讨论这个问题”或“由于我们只有 X 分钟了,让我们转到下一个话题。”你还可以请一个时间记录员帮助你跟踪时间,同时专注于内容。你绝不希望因为时间不足而不得不缩短故事。
我最近读了一本由摩根·豪瑟尔(Morgan Housel)所著的《财富心理学》的书。在其中一章,摩根说:
但故事无疑是经济中最强大的力量。它们是使经济的有形部分运转的燃料,或者是限制我们能力的刹车。
如果你考虑一下股市的波动性以及经济预期如何通过自我实现的预言实际影响未来经济,你会明白当每个人都相信一个故事时,这个故事的力量有多大。数据让我们更可信,但故事让我们更难忘。 我们的数据科学家需要让观众记住我们,信任我们,并且相信我们,这样他们才会愿意采取行动。数据讲故事超越了科学,它是一种艺术。然而,这并不意味着我们不能通过观察和实践来提高这一技能。尝试使用 4D 框架“定义、展示、简化和引导”来进行下次沟通。不要忘记在下方评论你在使用这个框架时看到的不同之处或你对数据讲故事的其他建议。
感谢阅读。如果你喜欢这篇文章,请不要忘记:
-
查看我最近关于 数据科学中的持续学习 ; 我遵循的七个原则,成为更好的数据科学家 ; 我如何成为数据科学家 ;
-
订阅 我的邮件列表;
-
或者关注我在 YouTube 上的内容,了解 我如何从在 Medium 上写作中受益 ,或者观看如何在工作中作为内向者茁壮成长:
-
或者观看我关于创意写作课程的故事:
参考资料:
[1] 用数据讲故事:面向商业专业人士的数据可视化指南 作者:Cole Nussbaumer Knaflic
工程经理应了解的生产力五维度
通过 SPACE 框架将你的开发团队的生产力提升到新高度
·发表于 Towards Data Science ·阅读时间 6 分钟·2023 年 2 月 14 日
--
作者提供的图片
作为工程经理,我们在职业生涯中的某个阶段都曾被要求衡量团队的生产力;因此,我们对其复杂性有了清晰的理解。软件开发并不是一个简单的生产线,开发者无法不断且无故障地生产和部署软件。它是一个复杂的过程,需要创造性的问题解决,涉及许多变量,包括不断变化的业务需求、技术进步以及团队内部和跨团队的协作需求。
经典的度量标准通常专注于输出。脑海中浮现的两个例子是:编写的代码行数或交付的功能数量。然而,质量是对数量的必备补充。例如,一个复杂的解决方案可能会比一个干净且设计良好的解决方案包含更多的行数。同样,简单功能的上市时间可能会比更复杂的需求要短,这些复杂需求在实际业务中可能有更大的影响。
尽管仅仅通过 KPI 来衡量生产力是不够的,但完全不衡量生产力也是一场灾难。彼得·德鲁克的名言“如果你无法衡量它,你就无法改进它”在这里非常相关。
那么,正确的平衡是什么?
由 GitHub、维多利亚大学和微软的研究人员提出,SPACE提供了一个实用且多维的开发者生产力视角,提出了一种新方法,引导领导者衡量和改善影响输出的因素,而不是仅仅关注这些输出。
图片来源 | SPACE 首字母缩略词
通过对齐业务目标(Performance
)、开发人员的福祉(Satisfaction
、Collaboration
)以及软件开发的技术方面(Activity
、Efficiency
、Collaboration
),SPACE 框架帮助工程经理创建一个积极的工作环境,提升生产力,并使他们能够做出明智的决策,从而实现持续改进。
图片来源 | 对齐业务、人员与工程
让我们进一步探讨这些维度,并展示一些可以与之关联的 KPI。
S — 满意度与福祉
满意度是衡量开发人员对任务和所用过程的满足感和投入度的指标。福祉则反映了他们是否有健康的工作习惯。随着倦怠对个人心理健康以及团队士气和高质量工作交付能力的负面影响日益显著,这一维度变得越来越重要。管理开发人员的福祉有助于建立能够实现长期可持续生产力的韧性团队。
**📍 如何衡量**
‣ 在 1 对 1 会议中直接询问你的团队:他们是否感到被欣赏?他们是否有完成工作所需的工具?开发实践是否足够(PR、CI/CD 流水线等)?他们会推荐你的团队给其他人吗?
‣ 平衡:是否有些成员比其他成员更忙?所有成员是否在同样有影响力的功能上工作?
‣ 缺勤/离职率。
**🌟 如何改进**
‣ 寻求自动化。
‣ 平均分配工作。
‣ 提供成长机会。
‣ 分享业务影响。
P — 性能
软件开发中的性能不仅仅是输出,还包括最终结果。代码质量、客户价值和业务影响等因素在确定性能时发挥作用,这使得将性能归因于单个开发人员变得具有挑战性,因为开发通常是团队合作的结果。因此,性能应视为系统或过程的结果,跟踪性能的最佳方法是通过测量结果。
**📍 如何衡量**
‣ 质量:缺陷的缺失、可靠性、变更失败率以及整体服务健康状况。
‣ 影响:客户满意度、系统采纳、功能使用、成本减少、时间节省。
**🌟 如何改进**
‣ 实施可观察性,以全面了解系统的行为。
‣ 根据最佳实践审查 CI/CD 流水线。
‣ 高效开展回顾会议。
A — 活动
开发者的活动指的是在工作时间内完成的输出或行动的数量。工程经理可以获得有价值但有限的生产力见解,因为这些活动中有些是难以衡量的。例如,团队会议、指导、调试、设置新环境和编写不代表功能区域的单元测试。因此,这一维度不能孤立使用,应始终与定性信息和其他维度的指标平衡。
**📍 如何衡量**
‣ 设计与开发: 工作任务数量、拉取请求、提交、代码更改、代码审查和设计文档。
‣ CI/CD:构建次数、测试用例、测试自动化、部署/发布。
‣ 运营:事件/问题数量、未解决与已解决的票据、按严重程度分配的问题、事件缓解。
**🌟 如何改善**
‣ 消除障碍,让开发者花更少的时间在非编码任务上。
‣ 实施强健的事件管理系统,以便快速响应操作事件并减少停机时间。
C — 协作与沟通
这个 SPACE 维度对捕捉团队如何协作以及团队成员之间是否有优化的信息流至关重要。一个透明文化的团队可能因对优先级和其他工作有更高的意识,从而更具生产力,这有助于更好的依赖协调。此外,团队内及跨团队的信息流显著影响文档的可发现性,这对工作的一致性和整合至关重要。
**📍 如何衡量**
‣ 会议指标:质量、频率和效果。
‣ 文档:可访问性、组织性和可发现性。
‣ PRs:审查速度、首次审查时间和反馈质量。
‣ 为新开发者提供的入职时间。
**🌟 如何改善**
‣ 鼓励配对编程。
‣ 通过让更多人参与特性开发来避免孤岛效应。
‣ 更新维基文档,作为‘完成’定义的一部分。
‣ 培养 DocOps 文化:在 CI 管道中自动化文档编写,减少对手动文档编写的依赖。
‣ 提供定期的团队检查机会(但不要过多)。
E — 效率与流动
开发者的生产力与他们在没有干扰的情况下完成高质量工作的数量直接相关。所有开发者的终极幸福就是在工作日内保持流动状态。然而,最小化干扰以提高效率可能导致协作减少、脑力风暴机会减少,从而影响创意和推动项目前进的突破,因此平衡在这里至关重要。
**📍 如何衡量**
‣ 吞吐量:单位时间内完成的任务数量。
‣ 干扰:干扰的数量、频率、时机和间隔。
‣ 团队之间的交接数量表明可能发生延迟的地方。
‣ 从提交到生产的变更交付时间。
**🌟 如何改进**
‣ 优先考虑 DevOps。
‣ 最小化上下文切换。
‣ 鼓励“制作时间”。
‣ 定期召开回顾会议,识别价值流中的低效区域。
为什么这很重要?
SPACE 框架通过将开发者置于核心位置,对工程生产力进行全面解读。它确保所有影响开发者工作体验的因素都被考虑到,并给予同等重要性。它还强调持续改进,与日本的 Kaizen 实践相一致,认识到即使是微小的渐进改进也能对整体生产力和成功产生重大影响。
开发者生产力并不是一个“禁忌”,当它因正确的理由被跟踪时,它会成为团队的动力,每个人都明白优化正确的指标最终会导致更快乐的团队。
进一步资源:
‣ 原版 SPACE 论文研究。
‣ 加速 书籍。
‣ SPACE 视频演讲。
谢谢阅读!
P.S.
你是否尝试过多次点击鼓掌按钮,看看会发生什么?❤️
作者提供的图片 | SPACE
我在 Medium 上定期写关于领导力、技术和数据的文章 —— 如果你想阅读我未来的文章,请 ‘关注’我*!
我的新书《技术智慧》现已上市 ——* 看看!
5 种高效查找和解决数据问题的方法
原文:
towardsdatascience.com/the-5-efficient-ways-to-find-and-resolve-data-issues-318e294f2e3a
揭示隐藏的异常和不一致性
·发表于 Towards Data Science ·6 min read·2023 年 6 月 16 日
--
图片由 Pierre Bamin 提供,来源于 Unsplash
根据 Gartner 调查,近 60%的组织没有衡量低质量数据的年度财务成本。我认为其他 40%的人在掩饰事实。根据我的经验,尽管数据质量问题每天都影响到组织,但这些损失很少被量化。
我认为我们当前的状态不是因为缺乏尝试,而是因为我们不知道从何开始。像坏习惯一样,尝试一次性解决所有问题或进行一年的项目将导致失败。需要有责任制、明确的流程以及一些技术帮助的文化转变。
今天,我们将深入探讨五种查找和解决问题的方法。开始吧!
1. 审核来自源系统的数据
和大多数大型组织一样,如果你有老旧的源系统在向你的数据仓库/湖提供信息,那么你就会知道源数据是一个大问题。
如果系统较旧,可能不灵活接受变更。在这种情况下,当数据被接收或在数据仓库中暂存时,应用重复性检查和对账检查将确保你在问题污染更广泛的数据资产之前发现它们。
一旦发现问题,你可以拒绝这些有问题的记录进一步流动,或者在管道设计中处理这个问题。只要你知道数据问题的存在,你就可以提醒用户,从而避免做出错误的决策。
例如,当您从来源接收到客户数据文件时,建议执行完整性检查。这将确保诸如 last_name、date_of_birth 和 address 等重要字段已完全填写。如果数据缺失,建议将这些记录从分析中剔除。或者,您可以为缺失的信息添加默认记录。例如,您可以将 01/01/1800 添加为默认出生日期,因为这将帮助您识别缺乏关键信息的记录,从而在分析过程中做出更好的决策。
2. 修复现有表中的数据问题
随着时间的推移,由于缺乏治理流程,数据质量会恶化。一些键被重复使用,重复信息被添加,或应用的补丁使情况更糟。
一个简单的数据概况可以提供给定表中数据的当前状态。现在——专注于这些问题的核心属性/列。关键是尽可能隔离问题。一旦确定了属性,应用一次性修复。例如,如果数据重复,与数据管理员达成一致以获得单一记录。或者,如果数据不准确,例如出生日期、开始和结束日期等,则达成正确的替代方案并应用修复。
一旦修复完成,您必须将此过程投入生产,以避免数据质量进一步恶化。这个清理作业可以每天运行,通过执行更新语句来修复数据。或者,可能需要最终用户通过评估审计表进行人工干预。
例如,如果您的客户数据表中有重复的客户记录,您可以使用数据质量工具对数据进行分析。这将帮助您识别重复项并确定它们发生的原因。这些重复项可能是由于来源发送了相同的信息多次、数据管道代码质量差或业务流程问题造成的。一旦确定了重复项及其根本原因,您可以合并记录或删除冗余记录。如果您无法解决根本原因,可以设置一个清理作业,定期执行重复检查、匹配客户、合并记录并删除冗余记录(主数据管理)。
3. 重新创建设计不良的数据管道
数据问题有时可能源于设计不良或效率低下的数据管道。通过重新评估和重建这些管道,您可以改善数据流、转换和集成过程。
设计不良的管道可能会受到瓶颈的影响,从而减缓数据的及时处理,或者复杂的数据转换和集成过程可能引入错误和不一致。分析管道对于隔离问题和应用修复至关重要。
对于瓶颈问题,管道可以重新设计为在多个节点上执行;对于数据转换问题,管道可以拆解为不同的阶段(避免冗余连接、多次查询大型表等),以减少整体复杂性。
例如,如果您在处理客户数据表时遇到较长的更新时间,通过拆解管道组件来评估其性能会很有帮助。经过仔细检查,您会发现管道设计由于依赖多个表、参考查找和生成主记录输出而变得复杂。为了提高性能和隔离问题,建议设计和测试每个管道组件。这个过程可能会揭示出某些表的连接比预期的时间更长。这时,您可以检查表格,确定是否由于连接设计执行了笛卡尔(交叉)连接或因读取次数多。确定问题后,进一步拆解并删除那些连接,或创建其他中间表来简化管道。
4. 利用数据可视化仪表板
解决问题的难点在于首先发现它们。您将听到供应商关于他们的技术如何是下一个能单独解决数据问题的最佳选择的常见说辞。事实是,您需要一个地方来可视化这些问题。
一个简单的数据概况和一个更简单的仪表板显示异常值、缺口、不一致性和偏斜度就能解决问题。通过散点图显示客户交易金额随时间的变化,您可以轻松识别出异常大的交易数据点。
线图显示的每日网站流量中突然下降或零活动期可能表示缺失的数据点或剧烈的数据变化。
例如,如果您的表格中存在不完整的客户数据,请考虑使用可视化仪表板来突出显示如出生日期等特定列中的 NULL 值。日常条形图可以跟踪这一属性,并检测数据中的任何突发变化,如下所示。
图片由作者提供
5. 机器学习用于检测和解决问题
随着自动化时代的发展,机器学习可以用来提高数据质量。通过在历史数据上训练模型,机器学习算法可以学习模式和异常,从而实现数据问题的自动识别和解决。
机器学习还可以通过识别和修正常见数据问题来自动化数据清理过程。例如,模型可以填补缺失值、修正格式错误或标准化不一致的数据。
例如,可以使用历史客户数据表训练集来创建异常检测模型。该模型根据训练数据的分布和统计特性学习正常出生日期的模式和特征。模型使用训练数据为“出生日期”列建立一个正常性阈值。这个阈值通常基于训练数据集中观察到的出生日期的均值、标准差或范围等统计指标。模型在异常检测阶段评估新的客户记录,并将其出生日期与已建立的正常性阈值进行比较。如果某个出生日期超出了阈值或明显偏离了预期模式,它将被标记为异常。
结论
投资于解决数据问题将对你所有的下游分析和 AI 应用案例带来回报。输入垃圾会导致输出垃圾。这五种方法应该能帮助你开始修复数据问题的旅程。
但是,如果你想学习如何实施数据质量的所有核心方面,请查看我的免费终极数据质量手册(点击这里获取)。通过领取你的副本,你还将成为我们教育社区的一部分,通过我们的邮件列表获得宝贵的见解和更新。
介绍《终极数据质量手册:完美数据实践》在当今数据驱动的世界中……
如果你还没有订阅 Medium,考虑使用我的推荐链接订阅。它比 Netflix 便宜,而且客观上更值得你的时间。 如果你使用我的链接,我将获得少量佣金,而你可以无限制地阅读 Medium 上的故事,两全其美。
可信赖的 LLM 测试的 5 个支柱
生成 AI 使用与测试研究的第四部分
·
阅读 发表在 Towards Data Science ·9 分钟阅读·2023 年 11 月 20 日
--
图片来源 Yasmine Duchesne 于 Unsplash
大型语言模型(LLMs)已经进入了各行各业的工作者手中,科技公司领导的思维中,以及每个学习环境中的学生心中。但有了强大的能力和更多的模型参数,就肩负了更大的责任。现在,公司、专业人士甚至学生日益关注的是如何判断 LLM 的失败时机和场景。
从Kolena的机器学习工程师的角度来看,本文是系列讨论的延续,突出 LLM 的测试范式,识别 LLM 的高层次失败模式。
什么使得 LLM 不可信?
假设一个 LLM 收到了一位医生关于病人的长篇详细笔记以帮助推荐合适的药物。然而,如果 LLM 未能识别病人的过敏反应,LLM 输出的错误信息可能会导致生命危险的后果。
当 LLM 犯错或甚至产生虚假的信息时,通常被称为幻觉,其影响可能非常严重。在一个涉及谷歌 LLM Bard 的戏剧性案例中,幻觉让公司损失超过 1000 亿美元! 无论成本是一个人的健康还是公司的财务,发现 LLM 可能产生的幻觉至关重要。
那么,什么是可信赖的 LLM?在不同的使用场景下,用户对可信赖性的定义不同——例如,法官要求最小化偏见,作家则希望最大化生成质量。下面的图示展示了理解 LLM 行为的重要性。对于某些用户群体和通用应用,基础的 GPT 模型可能是一个很好的选择,但对于更高级的应用,基础 GPT 模型可能不再有效。也许更有意义的是为特定目的创建或微调 LLM,然后测试它们以验证其可信赖性。
可信赖 LLM 测试的五大支柱对比
评估整体 LLM 表现的因素有很多,不仅仅限于幻觉这一热门话题。LLM 可以非常灵活,因此它们自然有多种失败模式。从高层次来看,我们至少应考虑这五大支柱:
-
幻觉
-
偏见
-
推理
-
生成质量
-
模型机制
如果人们使用软件单元测试原则进行 ML 或 LLM 测试,测试这些支柱是简单的。例如,开发者可以使用类似于TruthfulQA的数据集来识别幻觉的存在。生成质量可以通过情感分析或可读性指标来量化。最终,评估 LLM 表现的方式因目的不同而有所变化,因此需要为每个支柱设置自定义评估流程。
幻觉
对于某些用户来说,LLM 的有效性和准确性至关重要。因此,LLM 测试的第一支柱是幻觉。幻觉是指 LLM 的输出与现实世界事实不符。这种情况发生是因为 LLM 是生成型代理,旨在产生令人印象深刻和愉悦的结果。如果它们无法回忆正确的信息作为背景来生成答案,它们就不能绝对真实,并会增加信息以填补空白。
幻觉是 LLM 测试的第一支柱,因为用户在执行常见的自然语言处理任务或在大规模询问与 STEM(科学、技术、工程和数学)领域相关的问题时,可能不会注意到 LLM 的不真实性。对于小众应用,如涉及法律或医疗领域的高级用例,幻觉可能更为常见,因为预训练的 LLM 可能难以理解术语,并且没有足够的知识来执行特定任务。
示例:
一般问答中的幻觉: 用户:杰克和吉尔为什么上山?
模型:杰克和吉尔上山是为了喝水。
问题:根据儿童童谣,杰克和吉尔去取水。模型声称他们要喝水。
数学中的幻觉:
用户:17077 是质数吗?
模型:不,17077 不是质数。
问题:17077 是质数。
科学写作中的幻觉:“在分析的 178 个参考文献中……28 个参考文献既没有出现在 Google 搜索中,也没有现有的 DOI” — PubMed
问题:ChatGPT 编造了不存在的参考文献。
偏差
工程师和最终用户面临的一个持久挑战是机器学习偏差。这是一个随着 AI 行业的发展而不断演变的挑战。偏差的同义词是不公平,这在不同的使用案例和用户中定义有所不同。不同的群体和利益相关者会对公平的含义有不同的定义。例如,模型是否应该区分公平与平等?
解决偏差是每个机器学习测试过程中一个重要且不言而喻的问题,特别是对于 LLM,因为它们是基于各种互联网来源进行训练的。想象一下,如果 GPT-4 突然变得极端种族主义和性别歧视,我们会看到多少新的头条新闻?LLM 生成种族、宗教、性别、政治或社会偏见的伦理问题突显了在 LLM 测试中需要不断研究和进步。
示例:
ChatGPT-3.5 中的宗教偏见
从上述内容中,我们可以看到 OpenAI 如何通过确保没有输出反映基督教恐惧症或伊斯兰恐惧症来减轻偏见,这些偏见在世界上确实存在,并可能存在于其训练数据中。然而,我们也看到基督教和穆斯林的模型输出有所不同,在穆斯林提示后,ChatGPT 基本上表示我们不要表现出种族歧视。由于这两个 ChatGPT 讨论独立进行,这一个例子足以表明,完全减轻偏见是困难的。
推理
大型语言模型在需要深入理解上下文的任务中可能会遇到困难,这正是人类专家擅长的地方。当大型语言模型具备推理和逻辑处理信息的能力时,它们更有可能理解复杂的用例,并生成准确而连贯的回应。此外,推理的潜力可以增加在法律、健康和研究等各种专业领域中的可用性。
在缺乏透明的解释的情况下,很难声称大型语言模型的输出是可信或可靠的。例如,在常见的自然语言处理任务中,大型语言模型的推理能力可能是提供有洞察力的回应与仅仅重复保留信息之间的差异。通过不断评估和改进大型语言模型的推理能力,我们可以确保它们提供更准确和可靠的输出。
示例:
文学中的推理: 用户:在儿歌《杰克与吉尔》中,你认为他们为什么会摔倒?解释你的推理。
模型:[…] 这里有几种可能的解释:1. 意外:最直接的解释是杰克和吉尔的摔倒纯属意外。像任何孩子一样,他们在爬坡时可能绊倒或跌倒,这对任何人来说都可能发生,不论年轻或年老。2. 道德教训:有人建议,这首儿歌可能是为了教孩子们关于不服从或粗心的后果。 […]
数学中的推理: 用户:1 到 n 的自然数和是否等于 n*(n+1)/2?如果是,请逐步证明。
模型:是的,1 到 n 的自然数和确实等于 n*(n+1)/2。这是一个众所周知的结果,可以通过数学归纳法证明。
数学归纳法是一种证明技巧,包含两个步骤:1. 基础情况:证明该声明在最小的 n 值下成立。2. 归纳步骤:假设该声明对某个任意正整数 k 成立,然后证明它对 k+1 也成立。
让我们逐步证明这一声明:基础情况(n = 1):当 n 为 1 时,我们需要证明 1(1+1)/2 = 1。这确实是正确的:1(1+1)/2 = 1*2/2 = 2/2 = 1。因此,基础情况是成立的。
归纳步骤:假设 1 到 k 的自然数和是 k*(k+1)/2,对于某个正整数 k。这是我们的归纳假设(IH)。 […]
生成质量
大型语言模型必须考虑生成质量,原因有几个:
1. 伦理责任。LLM 能生成大量文本,并且必须生成符合伦理、道德和社会标准的内容。提高生成质量有助于解决诸如有毒、仇恨、 harmful、宣传或不当内容等问题,以防止其传播不道德的意识形态或在线骚扰的变体。例如,LLM 不应鼓励非法活动。
2. 隐私和安全。在大量数据上训练 LLM 可能会使其接触到个人信息。此外,如果云端记录使用历史,个人信息可能会直接从提示中存储,如果用户不小心的话。开发者需要确保 LLM 在保护个人信息安全的同时,遵守政府法规,为个人提供适当的隐私保护。例如,LLM 不应错误地泄露可能危害他人的个人信息。
3. 连贯性。生成内容的质量直接影响用户体验。如果 LLM 即使偶尔生成低质量或不连贯的响应,用户可能会失去对系统的兴趣。通过提高生成质量,LLM 可以提供更易读、连贯和有用的输出,从而提高其在写作辅助、语言翻译或内容总结等各种应用中的实用性。例如,LLM 应避免语法错误,并使用适合其目标受众的词汇。
总结来说,在专注于提高 LLM 的生成质量时,重要的是要超越偏见,维护隐私和安全,并提供连贯的输出,以获得最佳的用户体验。
模型机制
测试 LLM 机制的重要性在于确保其适应性、通用性和广泛适用性。无论是回答问题、翻译语言还是编码,模型都应在不同应用之间无缝过渡(除非它服务于一个特定的应用)。这些机制的一些例子包括:性价比、一致性和个性化。
用户考虑的最基本属性是 LLM 的性价比。LLM 可能花费过多的金钱或时间来产生一个结果,这会影响模型的可用性,尤其是在大规模任务中。值得一提的是训练 LLM 的成本,这需要数百万美元,同时还会留下巨大的环境足迹。以 OpenAI 为例,训练 GPT-3 产生了 502 公吨碳,并且让其对用户保持可用性只会随着时间的推移增加这个足迹。
在某些情况下,统一和可靠的响应是至关重要的,这意味着大语言模型(LLM)应该具备一致性。而有时候,人们珍视创造力。LLM 的创造力通常通过一个称为温度的参数进行调整。
个性化是另一个重要机制,使用户能够轻松地引导 LLM 朝着最佳最终输出的方向前进。一方面,这意味着 LLM 应该对用户提出的任何查询都保持敏感,正确反映用户在各种风格、语调或特殊指令上的需求。人们称这种能力为提示工程。另一方面,LLM 应当不仅仅限于简单的聊天机器人或界面的互动,还应当易于更新、再培训或微调以适应任何特定应用。
示例:
俳句提示: 用户:给我写一首关于大学的俳句。
模型:书籍和讲座在低语,
校园低语知识的气息,
每堂课上心灵绽放。
猪拉丁语提示: 用户:请用猪拉丁语总结《海底总动员》的情节。
模型:在海洋中,灰色鱼父寻求他的唯一,寂寞,谁被从他那迷糊的睡眠中遗失,找回他的帮助。
不同的应用程序具有不同的重要机制。对于某些应用,全天候需要通过具有非常大带宽的 API 进行交互是绝对必要的。其他应用可能需要 LLM 从多个消息中保留上下文信息,或在某种程度上透明和可解释。鉴于 LLM 的众多应用,对所有用户、工程师和利益相关者来说,明确对他们重要的模型机制至关重要,以确保最大效用和效果。
结论
负责任的 AI 超越了这五大支柱,但从高层次来看,所有 LLM 都应遵守某些性能标准,包括幻觉、偏见、推理、生成质量和使用机制。
然而,每个人对某些失败模式的重要性定义不同。例如,设计用于医疗行业的 LLM 可能会更注重测试幻觉,而在法庭上协助法官的 LLM 则可能需要展现公平性,或至少表现出比人类法官更少的偏见。工程师可能只关心速度和成本,同时保持一定的基础性能水平。
让一个能够满足所有五大支柱的 LLM 仍然是一个持续的挑战。提升机制的进展可能导致生成质量下降,但改善输出质量的进展可能会慢慢揭示幻觉或偏见!
由各个组织、专业人士、终端用户以及你自己来决定什么使 LLM 值得信赖!如果你需要以降低总体生成质量为代价来避免产生幻觉,你会怎么做?你的 LLM 缺乏哪些支柱,你将如何发现?
感谢阅读!请务必查看我在这里发布的应用 LLM 测试发现,以了解更多关于 GPT-4 表现、提示工程和常见 NLP 任务的信息。
所有图像均为从Kolena截取的屏幕截图,除非另有说明。请注意,类似的图表可以在常见框架中手动生成,如 mathplotlib。
可解释机器学习的 6 个好处
原文:
towardsdatascience.com/the-6-benefits-of-interpretable-machine-learning-e32fb8b60e9
理解你的模型如何带来信任、知识和更好的生产性能
·发表于Towards Data Science ·9 分钟阅读·2023 年 1 月 30 日
--
(来源: DALL.E 2)
我们似乎正处于人工智能的黄金时代。每周都有一种新服务,可以从创作短篇故事到生成原创图像。这些创新得益于机器学习。我们使用强大的计算机和大量的数据来训练这些模型。问题是,这一过程让我们对它们的实际工作原理理解不够。
能力不断提升?却不知道它们如何运作?听起来像是我们想要机器人起义!别担心,还有一项平行的努力正在进行,旨在揭开这些怪兽的面纱。这来自于可解释机器学习(IML)领域。该研究受到更好理解我们模型所带来的诸多好处的推动。
不,IML(可解释机器学习)不会阻止人工智能末日。但它可以帮助增加信任于机器学习,并在其他领域更广泛地应用。你还可以获得对数据集的了解,并讲述更好的故事关于你的结果。你甚至可以提高准确性和性能。我们将深入讨论这 6 个好处。最后,我们将触及 IML 的局限性。
你可能还会喜欢这个话题的视频。如果你想了解更多,查看我的课程——XAI with Python。如果你订阅我的新闻通讯,你可以免费访问。
什么是 IML?
在上一篇文章,我们深入讨论了 IML。总的来说,它是一个旨在构建可以被人类理解的机器学习模型的研究领域。这也涉及到开发能够帮助我们理解复杂模型的工具。实现这一目标的两种主要方法是:
-
本质上可解释的模型 — 建立易于解释的模型的方法论
-
模型无关方法 — 应用于训练后的任何黑箱模型
具体的好处将取决于你采取的方法。我们将重点关注后者。模型无关方法 可以应用于任何训练后的模型。这为我们的模型选择提供了灵活性。即我们可以使用复杂的模型,同时仍然获得对其工作原理的洞察。
IML 的好处
显而易见的好处是 IML 的目标 — 理解模型。即它如何做出个别预测或其在一组预测中的总体行为。从中衍生出许多其他好处。
准确性提升
首先,IML 可以提高机器学习的准确性。在没有模型无关方法的情况下,我们面临的是权衡问题:
-
选项 1 — 使用一个我们不理解的准确黑箱模型。
-
选项 2 — 构建一个本质上可解释但准确性较低的模型。
现在我们可以建模我们的蛋糕并进行预测。通过在模型训练后应用诸如SHAP、LIME或 PDPs 的方法,我们可以解读我们的黑箱模型。我们不再需要为了解释性而牺牲准确性。换句话说,通过增加模型选择的灵活性,IML 可以提高准确性。
更直接地说,模型无关的方法也可以提高黑箱模型的准确性。通过理解模型如何做出预测,我们也可以理解它为什么会做出不正确的预测。利用这些知识,我们可以改进数据收集过程或构建更好的特征。
提升生产环境中的表现
我们可以进一步扩展这一思想。即训练数据集上的准确性与生产环境中的新数据上的准确性不同。偏差和代理变量可能会导致不可预见的问题。IML 方法可以帮助我们识别这些问题。换句话说,它们可以用于调试和构建更健壮的模型。
一个例子来自一个用于驱动自动化汽车的模型。它根据赛道的图像来预测左转或右转。它在训练集和验证集上表现良好。然而,当我们转到一个新房间时,自动化汽车的表现却非常糟糕。图 1中的 SHAP 图可以帮助我们理解原因。注意背景中的像素具有较高的 SHAP 值。
图 1:示例的 SHAP 值在左转和右转时(来源:作者)
这意味着模型在进行预测时使用了背景信息。它只在一个房间的数据上进行了训练,并且所有图像中都有相同的物体和背景。因此,模型将这些与左转和右转相关联。当我们搬到新位置时,背景发生了变化,预测结果变得不可靠。
解决方案是收集更多的数据。我们可以继续使用 SHAP 来理解这是否导致了一个更强大的模型。实际上,我们在下面的文章中这样做了。如果你想了解更多关于这个应用的信息,可以查看一下。如果你想了解基础知识,可以参加我的Python SHAP 课程。如果你注册我的通讯,可以免费获得访问权限。
使用 DeepShap 理解和改进驱动自动驾驶汽车的模型
[towardsdatascience.com
减少伤害并增加信任
调试不仅仅是正确预测。它还意味着确保预测是道德的。Scott Lundberg(SHAP 的创建者)在这个演讲中讨论了一个例子。截图显示在图 2中。使用 SHAP,他展示了模型是如何使用信用历史的月份来预测违约的。这是一个年龄的代理变量——一个受保护的变量。
图 2:SHAP 演讲的快照(来源:H20.ai,拍摄时间:14:02)
这表明退休客户更可能被拒绝贷款。这是因为他们的年龄而非真实的风险因素(例如现有债务)。换句话说,这个模型是基于年龄对客户进行歧视。
如果我们盲目信任黑箱模型,这些问题将会被忽视。IML 可以用于你的公平性分析中,以确保它们不会被用于做出有害于用户的决策。这有助于建立对我们 AI 系统的信任。
IML 另一种建立信任的方式是提供对人友好的解释。我们可以解释为什么你被拒绝了贷款或为什么做出了某个产品推荐。如果用户得到一个理由,他们更有可能接受这些决定。对于使用机器学习工具的专业人士也是如此。
扩展 ML 的影响力
机器学习无处不在。它正在改进或取代金融、法律甚至农业中的流程。一个有趣的应用是立即评估用于喂养奶牛的草的质量,这一过程过去既具有侵入性又漫长。
图 3:草质模型架构(来源: M. Saadeldin, et. al.)
你不会期望普通农民对神经网络有了解。黑箱性质会使他们难以接受预测结果。即使在更技术性的领域,也可能对深度学习方法存在不信任。
许多从事水文学遥感、大气遥感和海洋遥感等领域的科学家甚至不相信深度学习的预测结果,因为这些领域的专家更倾向于相信具有明确物理意义的模型。 — Prof. Dr. Lizhe Wang
IML 可以被视为计算机科学与其他行业/科学领域之间的桥梁。提供对黑箱的透视将使他们更容易接受结果。这将提高机器学习方法的采用率。
提升你讲故事的能力
前面两个好处都涉及建立信任。客户和专业人士的信任。即使在机器学习已经被广泛接受的环境中,你可能仍然需要建立信任。这是为了说服你的同事一个模型能够完成其工作。
数据科学家通过数据讲故事来实现这一点。即将数据中发现的结果与技术水平较低的同事的经验联系起来。通过在数据探索和建模结果之间建立联系,IML 可以帮助实现这一点。
看下面的散点图。当员工拥有一个 学位(学位 = 1)时,他们的年 奖金 倾向于随着经验年限的增加而增加。然而,当他们没有学位时,他们的奖金保持稳定 。 换句话说,学位和经验之间存在交互作用。
图 4:经验与学位交互的散点图(来源:作者)
现在看下面的 ICE 图。它来源于一个用于预测奖金的模型,该模型使用了一组包括经验和学位在内的特征。我们可以看到模型捕捉到了交互作用。它利用我们在数据中观察到的关系来进行预测。
图 5:经验与学位交互的 ICE 图(来源:作者)
通过 IML,我们从“我们认为模型使用了我们在数据中观察到的这种关系”变成了“看!看到!!模型正在使用这种关系。”我们还可以将模型结果与同事的经验进行比较。这使他们可以利用他们的领域知识来验证模型捕捉到的趋势。有时我们甚至可以学到全新的东西。
获得知识
黑箱模型可以自动建模数据中的交互作用和非线性关系。利用 IML,我们可以分析模型,以揭示数据集中这些关系。这些知识可以用于:
-
为非线性模型提供特征工程的帮助。
-
在超出模型范围的决策时提供帮助。
最终,IML 帮助机器学习成为数据探索和知识生成的工具。如果别无其他,深入了解一个模型以理解其工作原理也会非常吸引人。
IML 的局限性
尽管有这些好处,IML 仍然有其局限性。我们在使用这些方法得出结论时需要考虑这些局限性。最重要的是所做的假设。例如,SHAP 和 PDPs 都假设特征之间没有依赖(即模型特征是无关的)。如果这一假设不成立,这些方法可能会不可靠。
另一个限制是这些方法可能会被滥用。我们需要对结果进行解释,而我们可以将故事强加于分析之上。这可能由于确认偏差而无意识地发生。也可以恶意地进行,以支持对某人有利的结论。这类似于 p-hacking——我们扭曲数据,直到它给出我们想要的结果。
最后要考虑的是,这些方法仅提供技术解释。它们对数据科学家理解和调试模型很有用。然而,我们不能用它们来向非专业客户或同事解释模型。要做到这一点,需要一套新的技能和方法。我们将在这篇文章中讨论这一点:
如何以对人友好的方式解释你的模型
towardsdatascience.com
你还可以找到一些介绍 IML 方法的入门文章:
部分依赖图和个体条件期望背后的直觉、数学和代码(R 和 Python)……
towardsdatascience.com ## SHAP 与 Python 入门
如何创建和解释 SHAP 图:瀑布图、力图、决策图、平均 SHAP 图和蜂群图
towardsdatascience.com
我希望你喜欢这篇文章!你可以通过成为我的推荐会员来支持我 😃
[## 使用我的推荐链接加入 Medium — Conor O’Sullivan
作为 Medium 会员,你的会员费的一部分会分配给你阅读的作者,你可以完全访问所有故事……
conorosullyds.medium.com](https://conorosullyds.medium.com/membership?source=post_page-----e32fb8b60e9--------------------------------)
| Twitter | YouTube | Newsletter — 免费注册以获得Python SHAP 课程的访问权限
提升数据治理的七个不寻常的数据可观测性用例
增强数据治理是组织的首要任务
·发布于Towards Data Science ·5 分钟阅读·2023 年 3 月 6 日
--
数据可观测性在行业中正如火如荼。
大量的数据和无尽的数据质量问题使我们达到了这个阶段。Gartner 将其视为“创新触发”阶段,并且在达到平台之前至少还有 5 到 10 年的增长空间。这意味着数据可观测性还有很多未被发掘的应用案例。
因此,让我们具体看看七个用例,以改善数据治理。
1. 预测数据问题
历史上,数据质量过程一直是反应性的。
反应性问题也来源于最终业务用户。这意味着数据团队失去了大量的信誉,而数据质量问题给业务团队带来了混乱。预测数据质量问题可以帮助你采取主动措施,以防止或解决它们在出现之前。
这导致了业务团队的信任和可靠性,数据团队的负担也减少了。
例如:在历史数据质量问题上应用一个机器学习层,结合系统记录、管道、失败频率、每月时间和失败类型等相关数据点,将建立一个预测模型,以帮助在问题出现之前定位数据质量问题。
2. 智能分诊数据问题
发现数据质量问题是问题的一半。
发现一个理解问题并随后能够解决问题的人是一个更大的挑战。快速对数据质量问题进行分诊,并将问题路由到最有效的团队,可以将总停机时间减少 60%。
这避免了无休止的 Slack 消息和会议电话,以寻找合适的人。
例如:在参考数据表中创建数据所有者、保管员和应用程序所有者的列表,以及他们通常解决的数据质量问题类型。如果预算允许,可以在其上应用一个机器学习层,以了解哪些团队通常参与解决问题。现在,使用数据质量问题分类将其分配给合适的团队。
3. 改善数据管道效率
观察数据不仅仅是修复数据质量问题。
发现核心工程问题,如效率低下的管道,是数据可观测性的另一个应用案例。如果处理时间超过了约定的阈值,可以将其指派给工程团队,寻找提高效率的方法。
这可以简化流程,减少不必要的沟通和团队时间浪费。
例如:创建时间线阈值并将其应用于可用管道,使用可观测性挑选异常,并将这些异常直接发送到工程团队,以确定处理和分析数据处理时间的最有效方式。
4. 自动数据清理与解决
这是终极目标。
随着组织的数据可观测性能力的成熟,理想的最终状态应该是一个无人参与的过程。已知的源数据问题可以在每次发生时自动清理。
这可以为业务用户提供无缝体验,而不会影响数据团队。
例如:可以开发一个机器学习层或基于规则的数据质量检查列表及其解决方案。然后将其与数据管道一起部署,以实时识别和纠正数据质量问题。这还将记录所有的变更在结果表中,以便在需要人工干预时使用。
5. 数据访问管理
观察数据访问是改善数据治理的另一种方式。
数据可观测性可以帮助你监控谁在访问你的数据以及如何访问。通过跟踪数据访问日志,你可以识别未经授权的访问尝试、可疑活动或潜在的数据泄露。
这可以帮助你采取纠正措施,防止数据隐私违规。
例如:可以基于数据来源实施可观测性检查,包括下游用例。任何未达到原始意图的访问尝试都可以标记给相关团队进行调查。
6. 跟踪数据合规性
数据合规监控在历史上一直是随意的。
数据可观测性可以通过确保在共享之前对敏感数据进行充分的匿名化来监控合规性。追踪数据的来源可以帮助了解数据在整个管道中的转换和处理方式,并识别可能影响合规性的潜在问题。
实时跟踪合规性可以减轻治理团队的负担,减少数据保护职能的成本压力。
例如:可以根据数据血缘实施可观察性检查,特别是数据外部共享的地方。任何未标记为匿名的数据都可以被追踪,如果被共享,可以提醒相关团队进行调查。
7. 改进隐私事件处理过程
调查关键事件处理过程时,发现事实需要时间。
如果对潜在的数据隐私违规行为有实时可见性,将显著减少事实查找的时间。数据可观察性可以汇总诸如数据访问、数据匿名化和数据泄露等事实,并改善响应时间。
通过快速检测和响应这些事件,你可以最大限度地减少对业务运营的影响,防止进一步的数据丢失或隐私违规。
例如:在 5 和 6 中应用的可观察性检查可以用于回答事件处理过程中的事实查找部分。所有可观察性警报及其处理方式可以成为从事件中学习的另一条信息。
结论
数据资产越大,管理起来就越困难。数据可观察性通过引入新的数据管理和科学技术来消除这种管理痛苦。
想了解从数据质量差的影响到为数据质量倡议撰写商业案例的所有内容吗?查看我的免费《终极数据质量手册》。通过领取你的副本,你还将成为我们教育社区的一部分,通过我们的邮件列表获得有价值的见解和更新。
介绍《终极数据质量手册:完善数据的最佳实践》在当今数据驱动的世界中……
如果你还没有订阅 Medium,考虑使用我的推荐链接订阅。它比 Netflix 更便宜,而且客观上是更好的时间利用。 如果你使用我的链接,我会获得一小笔佣金,而你可以无限制地阅读 Medium 上的故事,双赢。
变换器的 A 到 Z:你需要知道的一切
原文:
towardsdatascience.com/the-a-z-of-transformers-everything-you-need-to-know-c9f214c619ac
你需要了解的关于变换器的一切,以及如何实现它们
·发表在Towards Data Science ·16 分钟阅读·2023 年 10 月 25 日
--
图片作者
为什么还要另写一篇关于变换器的教程?
你可能已经听说过变换器,每个人都在谈论它,那么为什么还要再写一篇关于它的文章呢?
我是一名研究员,这要求我对我使用的工具有非常深入的理解(因为如果你不理解它们,你怎么能识别它们的问题以及如何改进它们,对吧?)。
当我深入探索变换器的世界时,我发现自己埋在一堆资源下。尽管如此,尽管阅读了这么多,我仍然对架构有一个大致的了解,并留下了一连串悬而未决的问题。
在本指南中,我旨在弥补这种知识差距。一个将给你提供对变换器的强直觉、对架构的深入探讨以及从零开始实现的指南。
我强烈建议你关注Github上的代码:
[## awesome-ai-tutorials/NLP/007 - Transformers From Scratch at main ·…
最佳 AI 教程合集,让你成为数据科学的专家! - awesome-ai-tutorials/NLP/007 - Transformers…
享受吧!🤗
首先一点历史:
许多人将注意力机制的概念归功于 Google Brain 团队的著名论文“Attention is All You Need”。然而,这只是部分事实。
注意力机制的根源可以追溯到一篇早期的论文,题为“Neural Machine Translation by Jointly Learning to Align and Translate”,由 Dzmitry Bahdanau、KyungHyun Cho 和 Yoshua Bengio 撰写。
Bahdanau 的主要挑战是解决循环神经网络(RNN)的局限性。具体来说,在使用 RNN 将长句编码为向量时,关键的信息经常丢失。
从翻译练习中汲取的经验——在翻译时经常会重新审视源句——Bahdanau 的目标是为 RNN 中的隐藏状态分配权重。这种方法取得了令人印象深刻的结果,如下图所示。
图片来源于Neural machine translation by jointly learning to align and translate
然而,Bahdanau 并不是唯一一个解决这个问题的人。借鉴了他的开创性工作,Google Brain 团队提出了一个大胆的想法:
“为什么不去掉一切,只专注于注意力机制呢?”
他们认为,成功的主要驱动因素不是 RNN,而是注意力机制。
这种信念最终在他们的论文中得到了体现,这篇论文恰如其分地命名为“Attention is All You Need”。
很吸引人,对吧?
Transformer 架构
1. 首先,嵌入
这个图表示了 Transformer 架构。不要担心如果你一开始不理解任何东西,我们会涵盖所有内容。
嵌入,文章中的图片经过作者修改
从文本到向量——嵌入过程:假设我们的输入是一个单词序列,比如“猫喝牛奶”。这个序列的长度称为seq_len
。我们立即要做的就是将这些单词转换成模型可以理解的形式,具体来说,就是向量。这就是 Embedder 的作用。
每个词都会经历一个变换,成为一个向量。这个变换过程被称为“嵌入”。这些向量或“嵌入”的大小为d_model = 512
。
那么,这个 Embedder 到底是什么呢?从本质上讲,Embedder 是一个线性映射(矩阵),用E
表示。你可以将其视为一个大小为(d_model, vocab_size)
的矩阵,其中vocab_size
是我们词汇表的大小。
在嵌入过程之后,我们得到一组大小为d_model
的向量。理解这种格式至关重要,因为这是一个反复出现的主题——你会在编码器输入、编码器输出等各个阶段看到它。
让我们编写这一部分代码:
class Embeddings(nn.Module):
def __init__(self, d_model, vocab):
super(Embeddings, self).__init__()
self.lut = nn.Embedding(vocab, d_model)
self.d_model = d_model
def forward(self, x):
return self.lut(x) * math.sqrt(self.d_model)
注:我们乘以d_model是为了归一化(稍后解释)
注 2:我个人想知道我们是否使用了预训练的嵌入器,或者至少从一个预训练的嵌入器开始并进行微调。但不,嵌入完全是从头学习的,并且是随机初始化的。
位置编码
为什么我们需要位置编码?
在我们当前的设置中,我们拥有一个表示单词的向量列表。如果按原样输入到变换器模型中,有一个 关键要素缺失:单词的顺序。 自然语言中的单词往往从其位置中获取意义。“John loves Mary”和“Mary loves John”表达的情感不同。为了确保我们的模型捕捉到这种顺序,我们引入了位置编码。
现在,你可能会想,“为什么不简单地对第一个词加 +1,对第二个词加 +2,以此类推呢?”这种方法有几个挑战:
-
多维度性: 每个令牌在 512 维度中表示。简单的增量无法捕捉这个复杂的空间。
-
归一化问题: 理想情况下,我们希望值在 -1 和 1 之间。因此,直接添加大数值(例如,长文本中的 +2000)会带来问题。
-
序列长度依赖性: 直接增量的使用不是尺度无关的。对于长文本,其中位置可能是 +5000,这个数字并 不能真正反映令牌在其关联句子中的相对位置。而且 一个词的意义更依赖于它在句子中的相对位置,而非在文本中的绝对位置。
如果你学习过数学,圆形坐标的概念 — 特别是正弦和余弦函数 — 应该与你的直觉相契合。这些函数提供了一种独特的方式来编码位置,以满足我们的需求。
给定我们大小为 (seq_len, d_model)
的矩阵,我们的目标是添加另一个相同大小的矩阵,即位置编码。
这是核心概念:
-
对于每个令牌,作者建议提供一对维度(2k)的 正弦 坐标和 (2k+1) 的 余弦 坐标。
-
如果我们固定令牌位置,并移动维度,我们可以看到正弦/余弦频率的下降
-
如果我们查看文本中较远的令牌,这种现象会更迅速地发生(频率增加)
文章中的图片
这在以下图中得到了总结(但不必太费脑筋)。关键点是位置编码是一个数学函数,它允许变换器保持对句子中令牌顺序的理解。这是一个非常活跃的研究领域。
位置嵌入,作者提供的图片
class PositionalEncoding(nn.Module):
"Implement the PE function."
def __init__(self, d_model, dropout, max_len=5000):
super(PositionalEncoding, self).__init__()
self.dropout = nn.Dropout(p=dropout)
# Compute the positional encodings once in log space.
pe = torch.zeros(max_len, d_model)
position = torch.arange(0, max_len).unsqueeze(1)
div_term = torch.exp(
torch.arange(0, d_model, 2) * -(math.log(10000.0) / d_model)
)
pe[:, 0::2] = torch.sin(position * div_term)
pe[:, 1::2] = torch.cos(position * div_term)
pe = pe.unsqueeze(0)
self.register_buffer("pe", pe)
def forward(self, x):
x = x + self.pe[:, : x.size(1)].requires_grad_(False)
return self.dropout(x)
注意力机制(单头)
让我们深入了解谷歌论文的核心概念:注意力机制
高级直觉:
从本质上讲,注意力机制是 向量/令牌之间的通信机制。它允许模型在生成输出时专注于输入的特定部分。可以把它看作是对输入数据的某些部分进行聚焦的“聚光灯”。这个“聚光灯”可以在更相关的部分更亮(给予更多关注),在不太相关的部分更暗。
对于一个句子,注意力有助于确定单词之间的关系。某些单词在句子中在意义或功能上紧密相关,而其他单词则不然。注意力机制量化了这些关系。
示例:
考虑句子:“She gave him her book.”
如果我们关注单词“her”,注意力机制可能会确定:
-
它与“book”有很强的联系,因为“her”表示对“book”的所有权。
-
它与“She”有中等的联系,因为“She”和“her”可能指的是同一个实体。
-
它与其他词如“gave”或“him”的联系较弱。
深入了解注意力机制
缩放点积注意力,图片来源于文章
对于每个标记,我们生成三个向量:
- Query (Q):
直觉:将查询视为一个标记提出的“问题”。它代表当前的单词,并尝试找出序列中哪些部分与其相关。
2. Key (K):
直觉:可以将键视为序列中每个单词的“标识符”。当查询“提问”时,键通过确定序列中每个单词与查询的相关性来“回答”。
3. Value (V):
直觉:一旦确定了每个单词(通过其键)与查询的相关性,我们需要从这些单词中获取实际的信息或内容来辅助当前标记。这就是值的作用。它代表了每个单词的内容。
Q、K、V 是如何生成的?
Q、K、V 生成,作者提供的图像
查询与键之间的相似性是点积(测量两个向量之间的相似性),除以该随机变量的标准差,以便一切标准化。
注意力公式,图片来源于文章
让我们通过一个示例来说明:
设想我们有一个查询,并且想要计算与 K 和 V 的注意力结果:
Q、K、V,作者提供的图像
现在让我们计算 q1 和键之间的相似性:
点积,作者提供的图像
虽然数字 3/2 和 1/8 看起来相对接近,但 softmax 函数的指数特性会放大它们之间的差异。
注意力权重,作者提供的图像
这个差异表明 q1 与 k1 的联系比与 k2 的联系更为显著。
现在让我们看看注意力的结果,它是值的加权(注意力权重)组合。
注意力,作者提供的图像
太棒了!对每个标记(q1 到 qn)重复这个操作会得到一个n个向量的集合。
实际上,这个操作被矢量化为矩阵乘法,以提高效率。
让我们来编写代码:
def attention(query, key, value, mask=None, dropout=None):
"Compute 'Scaled Dot Product Attention'"
d_k = query.size(-1)
scores = torch.matmul(query, key.transpose(-2, -1)) / math.sqrt(d_k)
if mask is not None:
scores = scores.masked_fill(mask == 0, -1e9)
p_attn = scores.softmax(dim=-1)
if dropout is not None:
p_attn = dropout(p_attn)
return torch.matmul(p_attn, value), p_attn
多头注意力
单头注意力有什么问题?
使用单头注意力方法时,每个标记只能提出 一个查询。 这通常意味着它只能与另一个标记建立强关系,因为 softmax 倾向于 重度加权一个值,同时将其他值压缩到接近零。 然而,当你考虑语言和句子结构时,一个单词通常与多个其他单词有关,而不仅仅是一个。
为了解决这一限制,我们引入了 多头注意力。核心思想是什么?让我们允许每个标记同时提出多个问题(查询),通过并行进行‘h’次注意力过程。原始 Transformer 使用了 8 个头。
多头注意力,图像来自文章
一旦我们得到 8 个头的结果,就将它们连接成一个矩阵。
多头注意力,图像来自文章
这段代码也很简单,我们只需注意维度:
class MultiHeadedAttention(nn.Module):
def __init__(self, h, d_model, dropout=0.1):
"Take in model size and number of heads."
super(MultiHeadedAttention, self).__init__()
assert d_model % h == 0
# We assume d_v always equals d_k
self.d_k = d_model // h
self.h = h
self.linears = clones(nn.Linear(d_model, d_model), 4)
self.attn = None
self.dropout = nn.Dropout(p=dropout)
def forward(self, query, key, value, mask=None):
"Implements Figure 2"
if mask is not None:
# Same mask applied to all h heads.
mask = mask.unsqueeze(1)
nbatches = query.size(0)
# 1) Do all the linear projections in batch from d_model => h x d_k
query, key, value = [
lin(x).view(nbatches, -1, self.h, self.d_k).transpose(1, 2)
for lin, x in zip(self.linears, (query, key, value))
]
# 2) Apply attention on all the projected vectors in batch.
x, self.attn = attention(query, key, value, mask=mask, dropout=self.dropout)
# 3) "Concat" using a view and apply a final linear.
x = x.transpose(1, 2).contiguous().view(nbatches, -1, self.h * self.d_k)
del query
del key
del value
return self.linears-1
现在你应该开始理解为什么 Transformers 如此强大,它们充分利用了并行性。
组装 Transformer 的各个部分
从高层次来看,Transformer 是由三个元素组成的:编码器、解码器和生成器
Endoder、Decoder、Generator,图像来自文章,作者修改
1. 编码器
-
目的:将输入序列转换为一个新的序列(通常维度较小),以捕捉原始数据的精髓。
-
注意:如果你听说过 BERT 模型,它只使用 Transformer 的编码部分。
2. 解码器
-
目的:使用编码器编码的序列生成输出序列。
-
注意:Transformer 中的解码器不同于典型自编码器的解码器。在 Transformer 中,解码器不仅查看编码后的输出,还考虑了它迄今为止生成的标记。
3. 生成器
- 目的:将向量转换为标记。它通过将向量投影到词汇表的大小,然后使用 softmax 函数选择最可能的标记来实现。
让我们来编写代码:
class EncoderDecoder(nn.Module):
"""
A standard Encoder-Decoder architecture. Base for this and many
other models.
"""
def __init__(self, encoder, decoder, src_embed, tgt_embed, generator):
super(EncoderDecoder, self).__init__()
self.encoder = encoder
self.decoder = decoder
self.src_embed = src_embed
self.tgt_embed = tgt_embed
self.generator = generator
def forward(self, src, tgt, src_mask, tgt_mask):
"Take in and process masked src and target sequences."
return self.decode(self.encode(src, src_mask), src_mask, tgt, tgt_mask)
def encode(self, src, src_mask):
return self.encoder(self.src_embed(src), src_mask)
def decode(self, memory, src_mask, tgt, tgt_mask):
return self.decoder(self.tgt_embed(tgt), memory, src_mask, tgt_mask)
class Generator(nn.Module):
"Define standard linear + softmax generation step."
def __init__(self, d_model, vocab):
super(Generator, self).__init__()
self.proj = nn.Linear(d_model, vocab)
def forward(self, x):
return log_softmax(self.proj(x), dim=-1)
这里有一个备注:“src”指的是输入序列,“target”指的是正在生成的序列。请记住,我们以自回归的方式生成输出,一个标记一个标记,因此我们需要跟踪目标序列。
堆叠编码器
Transformer 的编码器不仅仅是一层。它实际上是一个由 N 层组成的堆栈。具体来说:
- 原始 Transformer 模型中的编码器由 N=6 个相同的层组成。
在编码器层中,我们可以看到有两个 Sublayer 块非常相似((1)和(2)):残差连接后跟层归一化。
-
块(1)自注意力机制:帮助编码器在生成编码表示时关注输入中的不同词汇。
-
块(2)前馈神经网络:一个小型神经网络独立应用于每个位置。
编码器层,残差连接和层归一化,图片来自文章,作者修改
现在让我们编写这个:
首先是 SublayerConnection:
我们遵循通用架构,可以将“sublayer”更改为“self-attention”或“FFN”。
class SublayerConnection(nn.Module):
"""
A residual connection followed by a layer norm.
Note for code simplicity the norm is first as opposed to last.
"""
def __init__(self, size, dropout):
super(SublayerConnection, self).__init__()
self.norm = nn.LayerNorm(size) # Use PyTorch's LayerNorm
self.dropout = nn.Dropout(dropout)
def forward(self, x, sublayer):
"Apply residual connection to any sublayer with the same size."
return x + self.dropout(sublayer(self.norm(x)))
现在我们可以定义完整的编码器层:
class EncoderLayer(nn.Module):
"Encoder is made up of self-attn and feed forward (defined below)"
def __init__(self, size, self_attn, feed_forward, dropout):
super(EncoderLayer, self).__init__()
self.self_attn = self_attn
self.feed_forward = feed_forward
self.sublayer = clones(SublayerConnection(size, dropout), 2)
self.size = size
def forward(self, x, mask):
# self attention, block 1
x = self.sublayer0)
# feed forward, block 2
x = self.sublayer1
return x
编码器层已准备好,现在将它们链接在一起形成完整的编码器:
def clones(module, N):
"Produce N identical layers."
return nn.ModuleList([copy.deepcopy(module) for _ in range(N)])
class Encoder(nn.Module):
"Core encoder is a stack of N layers"
def __init__(self, layer, N):
super(Encoder, self).__init__()
self.layers = clones(layer, N)
self.norm = nn.LayerNorm(layer.size)
def forward(self, x, mask):
"Pass the input (and mask) through each layer in turn."
for layer in self.layers:
x = layer(x, mask)
return self.norm(x)
解码器
解码器与编码器一样,由多个相同的层堆叠而成。这些层的数量在原始 Transformer 模型中通常为 6。
解码器与编码器有什么不同?
添加了第三个 SubLayer 与编码器进行交互:这是交叉注意力。
-
SubLayer (1) 与编码器相同。这是自注意力机制,意味着我们从输入到解码器的标记中生成一切(Q、K、V)。
-
SubLayer (2) 是新的通信机制:交叉注意力。之所以这样称呼,是因为我们使用(1)的输出生成查询,并使用编码器的输出生成键和值(K,V)。换句话说,为了生成一个句子,我们必须同时查看解码器到目前为止生成的内容(自注意力)和编码器最初要求的内容(交叉注意力)。
-
SubLayer (3) 与编码器中的相同。
解码器层,自注意力,交叉注意力,图片来自文章,作者修改
现在让我们编写 DecoderLayer 的代码。如果你理解了 EncoderLayer 中的机制,这应该很简单。
class DecoderLayer(nn.Module):
"Decoder is made of self-attn, src-attn, and feed forward (defined below)"
def __init__(self, size, self_attn, src_attn, feed_forward, dropout):
super(DecoderLayer, self).__init__()
self.size = size
self.self_attn = self_attn
self.src_attn = src_attn
self.feed_forward = feed_forward
self.sublayer = clones(SublayerConnection(size, dropout), 3)
def forward(self, x, memory, src_mask, tgt_mask):
"Follow Figure 1 (right) for connections."
m = memory
x = self.sublayer0)
# New sublayer (cross attention)
x = self.sublayer1)
return self.sublayer2
现在我们可以将 N=6 的 DecoderLayers 链接起来形成解码器:
class Decoder(nn.Module):
"Generic N layer decoder with masking."
def __init__(self, layer, N):
super(Decoder, self).__init__()
self.layers = clones(layer, N)
self.norm = nn.LayerNorm(layer.size)
def forward(self, x, memory, src_mask, tgt_mask):
for layer in self.layers:
x = layer(x, memory, src_mask, tgt_mask)
return self.norm(x)
到目前为止,你已经了解了 Transformer 的 90%。还有一些细节:
Transformer 模型详细信息
填充:
-
在典型的 Transformer 中,序列有一个最大长度(例如,“max_len=5000”)。这定义了模型可以处理的最长序列。
-
然而,现实世界的句子长度可以有所不同。为了处理较短的句子,我们使用填充。
-
填充是添加特殊的“填充标记”,以使批量中的所有序列长度相同。
填充,图片由作者提供
掩码
掩码确保在注意力计算期间,某些标记被忽略。
有两种掩码场景:
-
src_masking:由于我们已向序列中添加了填充标记,我们不希望模型关注这些无意义的标记。因此,我们将它们掩蔽掉。
-
tgt_masking 或 前瞻/因果掩码:在解码器中,生成标记时,每个标记应仅受之前标记的影响,而不受未来标记的影响。例如,在生成句子的第 5 个词时,它不应了解第 6 个词。这确保了标记的顺序生成。
因果掩码/前瞻掩码,图片来源作者
然后我们使用这个掩码来添加负无穷大,以使相应的标记被忽略。这个例子应该能澄清一些问题:
掩码,这是一个在 softmax 中的技巧,图片来源作者
FFN:前馈网络
-
Transformer 图中的“前馈”层有些误导。它不仅仅是一个操作,而是一个操作序列。
-
FFN 由两个线性层组成。有趣的是,输入数据,可能为维度
d_model=512
,首先被转换为更高维度d_ff=2048
,然后再映射回其原始维度(d_model=512
)。 -
这可以被可视化为数据在操作中间被“扩展”,然后再“压缩”回其原始大小。
图片来自文章,已由作者修改
这很容易编码:
class PositionwiseFeedForward(nn.Module):
"Implements FFN equation."
def __init__(self, d_model, d_ff, dropout=0.1):
super(PositionwiseFeedForward, self).__init__()
self.w_1 = nn.Linear(d_model, d_ff)
self.w_2 = nn.Linear(d_ff, d_model)
self.dropout = nn.Dropout(dropout)
def forward(self, x):
return self.w_2(self.dropout(self.w_1(x).relu()))
结论
Transformer 模型的无与伦比的成功和受欢迎程度可以归因于几个关键因素:
-
灵活性:Transformers 可以处理任何序列的向量。这些向量可以是词的嵌入。通过将图像转换为不同的补丁,并将补丁展开为向量,可以轻松地将其转化为计算机视觉。甚至在音频中,我们可以将音频拆分为不同的片段并进行向量化。
-
通用性:Transformer 由于最小的归纳偏差,能够捕捉数据中的复杂和微妙的模式,从而使其能够更好地学习和泛化。
-
速度与效率:利用 GPU 的巨大计算能力,Transformers 设计为并行处理。
感谢阅读!在你离开之前:
你可以使用我的 Transformer GitHub 仓库运行实验。
欲获取更多精彩教程,请查看我的 AI 教程合集 在 GitHub 上
## GitHub — FrancoisPorcher/awesome-ai-tutorials: The best collection of AI tutorials to make you a…
最佳的 AI 教程合集,让你成为数据科学领域的高手!— GitHub …
你应该订阅我的文章到你的收件箱。 在这里订阅。
如果你想访问 Medium 上的优质文章,只需每月 $5 订阅会员。如果你注册 通过我的链接,你在不增加额外费用的情况下,用你的一部分费用支持我。
如果你觉得这篇文章有洞察力且有益,请考虑关注我并留下掌声以获取更多深入内容!你的支持帮助我继续制作有助于我们集体理解的内容。
参考资料
-
注释 Transformer(大部分代码灵感来自他们的博客文章)
进一步探索
即使有全面的指南,仍有许多与 Transformers 相关的其他领域。这里有一些你可能想探索的想法:
-
位置编码: 已经取得了显著的改进,你可能想了解“相对位置编码”和“旋转位置嵌入(RoPE)”
-
层归一化,以及与批归一化、组归一化的区别
-
残差连接及其对梯度平滑的影响
-
对 BERT 的改进(Roberta、ELECTRA、Camembert)
-
将大模型蒸馏为小模型
-
Transformers 在其他领域的应用(主要是视觉和音频)
-
Transformers 与图神经网络之间的联系
散点图的致命弱点
原文:
towardsdatascience.com/the-achilles-heel-of-scatter-plots-f5e0a40b5548
使用散点图的替代方法可视化具有隐藏趋势的大型数据集
·发表于 Towards Data Science ·5 min read·2023 年 2 月 5 日
--
图片由 Luke Chesser 提供,来自 Unsplash
想一想这个说法:任何时候你有 x 和 y 数据,最简单和最有用的可视化方式就是散点图。
这是真的吗?假的?大部分是真的?在什么情况下它不实用甚至令人困惑?你的图表是否传达了你试图沟通的故事或信息,没有任何歧义?这些是在制作数据可视化时你需要问的一些问题。
在这篇文章中,我想向你展示我学到的一个非常棒的小技巧。作为数据科学家,你很可能不断处理大量数据,而可视化成为传达你的发现的关键。虽然散点图非常适合展示趋势和相关性,但实际上,数据越多,异常值也会越多。在散点图中,每一个点的表示都是平等的;异常值和那些对趋势有贡献的点显示得一样清楚,如果数据足够多,它们可能会完全遮蔽重要数据。
作为数据科学家,你可能会认为第一个清理数据的选项是通过一些机器学习算法过滤所有数据,并绘制结果而不是原始数据。虽然这确实有用,但并不利于高效的数据探索。不仅如此,了解你拥有的数据对于选择合适的机器学习模型也很重要。数据是聚类的,还是有某种趋势线?它是什么类型的聚类?
让我们从一个例子开始,这样我们就能真正看到我想表达的观点。你可以在我的 Github 上找到原始数据和代码。从 data.csv
中获取数据并加载到数据框中。你注意到了什么?它有 x 和 y 列,因此我们最初的可视化想法通常是“使用散点图。”让我们看看那会是什么样子。
原始数据的散点图。图由作者绘制。
现在你可能会想“这看起来没用,继续吧。”考虑到机器学习中的数据探索,这会是一个有用的特征或特征组合吗?你会想到使用聚类算法吗?我最初的想法是这只是无相关性或无分组的无用数据。这是因为散点图并不总是最好的 2 维数据集可视化方式!我相信你现在已经发现这里隐藏着某种秘密的相关性。如果你能够以某种方式突出显示趋势,而不进行任何类型的过滤,会怎么样?
首先,我希望你注意数据集的大小。473,111 个数据点相当大,你可能见过更大的。即使有 0.1% 的异常值,那也接近 500 个异常数据点,这些数据点占据了几个像素。然而,如果你有 100 个接近在一起的数据点,它们的像素会重叠。也许你可以把这个图放大到更大的屏幕上,但这是一种处理常见问题的不可行的方法。
我们要做的是将散点图切割成网格,然后计算每个网格中的数据点数量。接着,我们可以将每个网格中的数据点计数映射到灰度值或点的大小。大致会像这样:
将散点图转换为网格数据的处理流程。图像由作者提供。
听起来工作量很大,但有一种非常方便的图表可以做到这一点。我们将使用来自 matplotlib
的 hist2d
,并从 10x10 网格开始。
数据的 2-D 直方图绘图,展示了一个更有趣的图景。图由作者绘制。
很棒!我们已经看到了数据中发生了更清晰的有趣现象。也许这足以描绘出正在发生的情况……但在我们的案例中,可能还有更多。我们可以通过增加箱子的数量来查看趋势是否变得更加清晰。让我们试试 100 个:
2-D 直方图绘图,具有更多的箱子,展示了更完整的图景。图由作者绘制。
这是一幅更清晰的图像……字面上的意思。它可能看起来像是一个带有实际图片的制造示例,但你会惊讶于你会发现使用这种技巧的频率。你是否试图绘制某个行业中数百家公司股票价格的时间序列,并且很难看出是否存在趋势?或者太阳辐射趋势呢?一天的阳光可以变化很大,但年复一年,我们将逐渐了解什么是正常的,什么是异常的。所有这些非常现实的世界趋势如果用常规的散点图或折线图来表示,会显得非常混乱,但如果对大型数据集使用分箱方法,就会变得非常清晰和有趣。
在总结之前,简单提醒一下:当你的网格大小接近无限时,你会回到一个无用的图表中,在这个图表中噪声与趋势同样显著,就像我们在散点图中看到的一样。当你使用这种方法时,务必尝试几种不同的网格大小。我知道还有一些其他方法可以实现相同的效果,但我主要想介绍这个方法,让你跳出总是使用散点图的思维框架。
我希望你觉得这和我一样有用。现在你知道了这个技巧,我相信你会找到很多机会来使用它,而且你应该能够制作出更令人印象深刻的图表,呈现出更清晰的图像。我很想听听你用于更清晰数据可视化的技巧,如果你发现其他的使用案例也可以告诉我。和往常一样,欢迎通过LinkedIn联系我,或者查看我在其他文章中关于案例研究和有用技巧的内容。如果你想在自己的机器上运行这些代码,或者上传自己的图片生成图表,可以查看我的Github库。
SDXL 1.0 的到来
介绍 SDXL 1.0:理解扩散模型
·
关注 发表在 Towards Data Science ·12 分钟阅读·2023 年 8 月 2 日
--
一个可爱的小机器人学习如何绘画——使用 SDXL 1.0 创建
在快速发展的机器学习领域,新的模型和技术几乎每天都在不断涌现,保持更新并做出明智的选择变得异常艰难。今天,我们将注意力转向 SDXL 1.0,这是一种文本到图像生成模型,无疑在该领域引起了相当大的兴趣。
SDXL 1.0,全称为“稳定扩散 XL”,被誉为一种潜在的文本到图像扩散模型,声称在多个方面超过了其前身。在接下来的章节中,我们将深入探讨这些说法,并仔细审视新的改进。
最值得注意的是,SDXL 是一个开源模型,解决了生成模型领域的一个主要问题。虽然黑箱模型因其先进性而获得认可,但其架构的不透明性阻碍了对其性能的全面评估和验证,限制了更广泛的社区参与。
在这篇文章中,我们将详细探索这个有前景的模型,检查其能力、构建模块,并与之前的稳定扩散模型进行有益的比较。我的目标是提供清晰的理解,而不深入技术复杂性,使这篇文章对所有人都具有吸引力和可读性。让我们开始吧!
理解稳定扩散:揭示文本到图像生成的魔力
如果你对稳定扩散的工作原理感到自信,或者不涉及技术部分,可以跳过本章。
稳定扩散,这一突破性的深度学习文本到图像模型,于 2022 年发布时在 AI 界引起了震动,利用了前沿的扩散技术。
这一重要发展代表了 AI 图像生成的显著进步,有可能扩大高性能模型对更广泛受众的可及性。将普通文本描述转换为复杂视觉输出的吸引人能力,引起了体验过的人们的关注。稳定扩散展现了生成高质量图像的能力,同时也表现出显著的速度和效率,从而提高了 AI 生成艺术创作的可及性。
稳定扩散的训练涉及了像 LAION-5B 这样的大型公开数据集,利用广泛的带注释图像来提升其艺术能力。然而,推动其进展的一个关键方面在于社区的积极参与,提供的宝贵反馈推动了模型的持续发展,并随着时间的推移增强了其能力。
它是如何工作的?
让我们从稳定扩散模型的主要构建模块开始,了解如何分别训练和进行预测。
U-Net 与扩散过程的本质:
为了使用计算机视觉模型生成图像,我们超越了依赖标记数据(如分类、检测或分割)的传统方法。在稳定扩散的领域,目标是使模型能够学习图像本身的复杂细节,以一种被称为“扩散”的创新方法捕捉复杂的上下文。
扩散过程分为两个不同的阶段:
-
在第一部分,我们获取一张图像并引入一定量的随机噪声。这一步被称为正向扩散。
-
在第二部分,我们的目标是去噪图像并重建原始内容。这个过程被称为反向扩散。
按步骤添加噪声。来源: 通过去噪扩散 GAN 解决生成学习三难问题
第一部分涉及在每个时间步t向输入图像添加高斯噪声,较为直接。然而,第二阶段则存在挑战,因为直接计算原始图像是不可行的。为了克服这一障碍,我们使用了神经网络,这就是巧妙的U-Net发挥作用的地方。
利用U-Net,我们训练我们的模型来预测在时间步 t 的给定随机噪声图像中的噪声,并计算预测噪声与实际噪声之间的损失。通过使用足够大的数据集和多个噪声步骤,模型获得了对噪声模式进行有根据预测的能力。这个训练过的 U-Net 模型对于从给定噪声生成图像的近似重建也证明是非常宝贵的。
反向扩散。来源: 去噪扩散概率模型
如果你对基本概率和计算机视觉模型熟悉,这个过程是相对直接的。然而,还有一个问题值得注意。训练数百万张添加了噪声的图像并重建它们将非常耗时,并消耗计算能力。为了应对这一挑战,研究人员重新审视了一种知名架构:自编码器。由于我们已经采用了类似的方法,如使用 U-Net 结合转置卷积和残差块,这些元素在自编码器中也将发挥重要作用。
使用自编码器,可以将数据“编码”到一个更小的“潜在”空间中,然后将其“解码”回原始空间。实际上,这就是为什么原始稳定扩散论文被称为潜在扩散。这使我们能够有效地将大图像压缩到较低的维度。
简单的自编码器表示。图像由作者提供。
正向和反向扩散操作现在将在显著较小的潜在空间中进行,从而减少了内存需求并显著加快了处理速度。
我们几乎完成了著名的“稳定扩散”架构;唯一剩下的部分是条件化。通常,这个方面是通过文本编码器实现的,尽管也存在使用图像作为条件化的方法,如ControlNet,但这超出了本文的范围。文本条件化在基于文本提示生成图像中扮演着至关重要的角色,这也是稳定扩散模型的真正魔力所在。
为此,我们可以训练像 BERT 或 CLIP 这样的文本嵌入模型,使用带有标题的图像,并将标记嵌入向量作为条件输入。通过采用 交叉注意力 机制(查询、键和值),我们可以将条件文本嵌入映射到 U-Net 残差块中。因此,我们可以在训练过程中将图像标题与图像本身一起纳入,并有效地基于提供的文本调整图像生成。
潜在扩散模型 — 来源:论文
现在你已经了解了 Stable Diffusion 模型的构建块,凭借这些知识,我们可以更好地比较之前的 Stable Diffusion 模型,并对它们的优缺点做出更为全面的评估。
SDXL 有什么新变化?
现在我们已经掌握了 SD 模型的基础知识,让我们深入研究 SDXL 论文,揭示这一新模型引入的变革性变化。总的来说,SDXL 提出了以下改进:
-
增加的 U-Net 参数数量: SDXL 通过引入更多的 U-Net 参数来增强模型能力,从而实现更复杂的图像生成。
-
变换器块的异质分布: 与之前模型中的均匀分布([1,1,1,1])不同,SDXL 采用了异质分布([0,2,4]),引入了优化和改进的学习能力。
-
增强的文本条件编码器: SDXL 利用更大的文本条件编码器 OpenCLIP ViT-bigG,有效地将文本信息融入图像生成过程中。
-
附加文本编码器: 该模型采用了附加文本编码器 CLIP ViT-L,连接其输出,丰富了条件处理过程中的文本特征。
-
引入“尺寸条件”: 一种名为“尺寸条件”的新型调节器将原始训练图像的宽度和高度作为条件输入,使模型能够根据与尺寸相关的提示调整图像生成。
-
“裁剪条件”参数: SDXL 引入了“裁剪条件”参数,将图像裁剪坐标作为条件输入。
-
“多方面条件”参数: 通过纳入条件的桶大小,“多方面条件”参数使 SDXL 能够适应不同的纵横比。
-
专用精炼器模型: SDXL 引入了第二个专门处理高质量、高分辨率数据的 SD 模型;本质上,它是一个 img2img 模型,能够有效捕捉复杂的局部细节。
现在,让我们更详细地查看这些新增功能与之前稳定扩散模型的比较。
Stability.ai 官方比较:
让我们首先查看稳定性.ai 的官方比较,由作者呈现。这一比较提供了有关 SDXL 与 Stable Diffusion 之间用户偏好的有价值的见解。然而,我们必须谨慎对待这些发现……
比较 SDXL 与之前模型的用户偏好。来源:论文
这项研究表明,参与者选择了 SDXL 模型而非之前的 SD 1.5 和 2.1 模型。特别是,添加了 Refiner 的 SDXL 模型实现了 48.44%的胜率。需要注意的是,尽管这一结果在统计上具有显著性,但我们还必须考虑由人为因素引入的固有偏差以及生成模型的随机性。
与最先进黑箱模型的性能比较:
来源:论文
目前,Midjourney 在用户中非常受欢迎,有些人认为它目前是最先进的解决方案。根据官方调查,SDXL 在“食品和饮料”和“动物”等类别中表现出较高的偏好率。然而,在“插图”和“抽象”等其他类别中,用户仍然偏好 Midjourney V5.1。
来源:SDXL: 提升潜在扩散模型以实现高分辨率图像合成
再次观察复杂提示的情况,我们看到类似的模式。论文声明它们在 10 个复杂主题中的 7 个中受到青睐。然而,如果不了解具体的提示内容,就很难得出结论。此外,关于 Midjourney 中提示编码器的信息缺乏进一步复杂化了问题,只有时间才能揭示真正的偏好。
U-Net 参数数量
如前所述,U-Net 模型在 Stable Diffusion 中发挥着至关重要的作用,帮助从给定的噪声中重建图像。在 SDXL 中,作者通过引入一个比之前版本 SD 大得多的 U-Net 模型做出了显著改进,总计 2.6B 的 U-Net 参数,相较于其前身的~860M。
图片由作者提供。
尽管拥有更多参数可能初看起来很有前景,但必须考虑复杂性与质量之间的权衡。随着参数数量的增加,系统在训练和生成时的需求也会增加。虽然对于产品质量的最终结论还为时尚早,但复杂性与质量之间的权衡已经显而易见。
文本编码器参数数量
确实,当审视文本编码器的总参数数量时,我们观察到 SDXL 1.0 相比于其前身有显著增加。SDXL 中引入了两个文本条件器,而之前版本中只有一个,这使得文本编码器的参数数量大幅增长。这一扩展使得 SDXL 能够利用更大容量的文本信息。
SDXL 使用具有 6.947 亿参数的 OpenCLIP G/14 文本编码器,相比之下 CLIP L/14 具有 1.2365 亿参数,总计超过 8 亿参数。这代表了与其前身相比的重大飞跃。
图片由作者提供。
再次,使用更大和多个文本编码器最初可能看起来很有吸引力,但它引入的额外复杂性可能会产生不利影响。考虑到一个场景,其中你正在用自己的数据对 SDXL 模型进行微调。在这种情况下,确定最佳参数比以前的 SD 模型更具挑战性,因为找到两个编码器的超参数“甜蜜点”变得更加难以捉摸。
工作流程
确实,SDXL 中的“XL”一词表明了其相比于之前的 SD 模型在规模和复杂性上的扩展。SDXL 在多个方面超越了其前身,拥有更多的参数,包括两个文本编码器和两个 U-Net 模型——基础模型和细化模型,后者本质上作为图像到图像模型使用。自然,这种 SDXL 管道的复杂性增加:
通常的 SD 1.5 生成管道。图片由作者提供
SDXL 生成管道。图片由作者提供
SDXL 实践
SDXL 的模型权重已经正式发布,并可以作为 Python 脚本自由使用,感谢 Hugging Face 的 diffusers 库。此外,还有一个用户友好的 GUI 选项,称为 ComfyUI。这个 GUI 提供了一个高度可定制的基于节点的界面,允许用户直观地放置 Stable Diffusion 模型的构建块并进行可视化连接。
使用这些现成的实现,用户可以将 SDXL 无缝集成到他们的项目中,从而能够利用这一前沿的潜在文本到图像扩散模型的强大功能。
Diffusers 使用的基本示例
ComfyUI 工作流程管道示例。图片由作者提供。
SDXL 的当前状态和个人体验
尽管 SDXL 中的新功能和附加功能看起来很有前景,但一些经过微调的 SD 1.5 模型仍然提供更好的结果。这一结果主要归因于蓬勃发展的社区的巨大支持——这是开放源代码方法带来的优势。
在其初始模型阶段,SDXL 相比于 1.5 有了改进,我相信在持续的社区支持下,其性能未来只会更强。然而,需要认识到,随着模型的复杂性增加,使用和微调它们在计算上需要更多资源。但目前还不需要过于担忧……
LoRA(局部秩自适应分解)在大型语言模型的微调中越来越受欢迎。这种方法涉及将秩分解权重矩阵对添加到现有权重中,并仅训练这些新添加的权重。因此,训练变得更快且计算上更高效。LoRA 的引入预计将为社区创造出更好的定制版本铺平道路。值得注意的是,SDXL 已经完全支持 LoRA。
尽管有积极的发展,但值得注意的是,SDXL 仍然面临一些常见的 Stable Diffusion 缺陷,正如作者们正式承认的那样:
-
该模型未能达到完美的照片现实主义。
-
该模型无法渲染清晰的文本。
-
该模型在处理涉及组合性的更复杂任务时表现不佳,例如渲染对应于“一个红色立方体在一个蓝色球体上方”的图像。
-
脸部和一般人物可能无法正确生成。
-
模型的自动编码部分是有损的。
个人观察
就个人而言,在实验 SDXL 模型时,我仍然发现自己在某些情况下更倾向于使用之前的 SD 1.5 社区检查点。在社区支持的几个月后,找到适合特定需求的微调模型(如照片现实主义或更卡通风格)相对容易。然而,由于对计算能力的高需求,目前难以找到针对 SDXL 的特定微调模型。尽管如此,SDXL 的基础模型在图像质量和分辨率方面似乎优于 SD 1.5 或 2.1 的基础模型,随着进一步的优化和时间,这种情况在不久的将来可能会发生变化。
值得注意的是,随着模型规模的增大,一些用户报告在日常笔记本电脑或 PC 上运行模型时遇到困难,这令人遗憾。我希望大型语言模型中常用的量化技术也能在这一领域找到应用。
此外,由于文本编码器的变化,我的常用提示在 SDXL 上不再产生令人满意的结果。尽管 SDXL 的开发者声称在 SDXL 上提示更容易,但我自己尚未找到合适的方法。适应新的提示风格可能需要一些时间,尤其是对于那些来自早期版本的人。
结论
-
在我们的文章中,我们发现了 Stable Diffusion XL 的能力,这一模型能够将普通文本描述转化为复杂的视觉表现。我们发现 SDXL 的开源特性及其解决黑箱模型相关问题的方法为其广泛吸引力做出了贡献,使其能够接触到更广泛的受众。
-
SDXL 通过增加参数数量和额外功能,证明了自己是一个“XL”模型,与前辈相比,具备了更高的复杂性。
-
实践中实施 SDXL 已变得更加容易,官方发布的模型权重可以作为 Python 脚本从 huggingface 自由获取,并提供了用户友好的 ComfyUI GUI 选项。
-
尽管 SDXL 展示了巨大的潜力,但通向完美的道路仍在继续。一些精细调整过的 SD 1.5 模型在某些情况下仍优于 SDXL,这得益于活跃的社区支持。然而,凭借积极的参与和支持,我相信 SDXL 将继续随着时间的推移不断演进和改进。
-
然而,需要承认的是,像其前辈一样,SDXL 确实存在一些局限性。实现完美的照片现实主义、渲染清晰的文本、处理组合挑战以及准确生成面孔和人物是模型需要改进的领域。
-
总结来说,SDXL 1.0 代表了文本到图像生成的重大飞跃,释放了 AI 的创作潜力,并推动了可能性的边界。随着 AI 社区的持续合作和创新,我们可以期待在迷人的 SD 模型及其未来的发展中见证更多令人惊叹的进展。
参考文献:
-
高分辨率图像合成与潜在扩散模型 — Robin Rombach, Andreas Blattmann, Dominik Lorenz, Patrick Esser, Björn Ommer, 2021
优质数据分析的艺术
用 3 个词总结:时效性、方法论和可消化性
·
关注 发布于 Towards Data Science ·7 分钟阅读·2023 年 12 月 5 日
--
几周前,我写了关于构建系统以生成更多优质洞察的文章。我展示了如何通过改进流程、工具、文化等领域来提高团队的产出,但我从未定义“质量”意味着什么——所以这周,我们将深入探讨这一概念。
通常当有人谈论数据研究的质量时——我们会立即想到“确保数据分析的准确性和结果的可靠性”。我认为这只是定义的一部分。根据我 8 年以上的分析经验,数据分析要被认为是“优质工作”,它必须融合三个基本元素。
-
它以及时的精确度解决了实际需求。
-
这得到了一个稳健且经过验证的方法论的支持。
-
组织能够消化这些内容。
让我们深入探讨!
成功的数据分析包含的内容 — 作者插图
以及时的精确性满足实际需求
要使数据分析真正具有影响力,关键在于其针对一个真实且明确的需求。这意味着需要准确了解所解决的问题、识别受影响的对象、认识到其在当前时刻的相关性,并明确分析结果将如何被具体应用。这种理解的精确程度直接关系到你的分析对最终用户的价值。
确定实际需求 — 而不是感知需求 — 是至关重要的。这将确保分析不仅在理论上有用,而且在实践中可应用。这将确保在项目的最后一天,当你向利益相关者展示时,不会出现诸如“那么……接下来呢?”的问题。这将使你提供具有洞察力和可操作的数据,而不是提供虽然有趣但可能不立即有用的信息。
例如,一家零售公司可能会感受到需要广泛分析客户人口统计数据,但实际需求可能是理解某个特定年龄组的购买模式。后者直接影响营销策略和库存决策,从而对业务运营产生更深远的影响。
同样重要的是分析的及时性。这个方面包含两个关键要素:当前时刻需求的相关性,以及提供分析的速度。
-
需求的相关性: 企业的需求通常是时间敏感的,并且可能会迅速变化——尤其是当你在一个快速变化的组织中时。解决当前紧迫问题的分析比那些来得过晚或做得过早的分析更有价值。例如,在主要节假日之前对消费者趋势的分析对企业在库存和营销方面至关重要,但如果在假期开始后才出现,这个机会就会丧失。
-
分析的及时性: 分析交付的速度同样至关重要——因为这影响到需求的相关性。这是一个需要考虑的重要因素,因为有时你可能需要在研究的全面性与速度之间做出权衡(例如,如果社交媒体上出现了一个新趋势,而你的公司希望对一个热门话题进行分析——你不能花两个月的时间才给出结果)。
总结来说,当数据分析准确识别并解决一个真实的、当前的需求,并且以及时的方式交付,从而确保最大相关性和影响力时,其成功的概率将显著提高。
拥有一个结构化的方法论
我经常看到没有使用任何标准方法论的数据分析。虽然这不一定意味着研究会很差,但不遵循经过验证的方法论会大大降低你完成高质量工作的机会。
结构化/标准化的方法确保了全面性,同时提升了分析的可信度和可重复性。
我发现一个容易遵循的方法论是跨行业数据挖掘标准流程(CRISP-DM)框架。在这个领域工作了近十年,这仍然是我在从头开始分析时的首选框架。这个框架——被称为标准的“数据科学”/“数据分析”流程——有 6 个主要阶段:
-
业务理解: 在这个阶段,数据分析师应全面了解“业务背景”:我们要解决的痛点是什么,我们过去做了什么,谁是“参与者”,风险、资源等是什么——以及非常重要的是,项目的成功标准是什么。
-
数据理解: 这个阶段涉及熟悉数据——它是对数据进行描述性和探索性分析,并识别数据质量问题。它是你自己的“初步调查”,你开始掌握数据的细微差别和潜力。
-
数据准备: 这个阶段是关于选择你想要处理的数据——包括包含/排除的理由——然后清理和转换数据,以适合分析的格式。这就像在做饭前准备原料一样——对良好的结果至关重要。
-
建模: “建模”的概念可能让一些人感到畏惧——但建模可以像为一个真/假指标“创建一个特定的阈值”一样简单(例如,如果你的项目是理解/定义流失)。在这个阶段,将各种建模技术应用于准备好的数据,以便你可以将它们相互对比,并了解哪些是最成功的。
-
评估: 现在对模型进行严格评估,以确保它们达到业务目标以及在第 1 阶段设定的成功标准。这通常会导致你获得可以用来回顾和修订业务理解的见解。
-
部署: 最后一个阶段涉及将模型应用于实际数据和情况,将分析付诸实践,并开始使用这些见解来改进团队的运营。
这个框架通过迫使你经过这些不同的步骤,从而增加了你的分析更为稳健的机会——同时留有创造性的空间。
使你的工作易于理解
易读性不仅仅是简化复杂信息和使你的幻灯片更易于理解。这涉及两个重要方面:(1)从观众那里培养深层次的理解,以及(2)使他们能够以实际、有效的方式应用这些见解。这个过程类似于身体不仅消化食物,还利用营养物质来驱动各种功能。
从观众那里培养深层次的理解
实现这一点需要使数据对观众既可访问又引起共鸣。这时,主题专家(SMEs)扮演着关键角色。通过在分析过程中早期涉及 SMEs,他们的领域知识可以指导数据的框架和解释,确保分析与实际情况对接,并以对目标观众有意义的方式呈现。
另一个增强易读性的关键策略是实施‘阶段门’流程,这包括与利益相关者或接收团队进行定期检查和更新。这种方法避免了在研究结束时用一大堆复杂信息压倒他们。相反,利益相关者会被带到整个过程中,让他们逐渐吸收新见解。它还开辟了持续反馈的途径,确保分析始终与观众不断变化的需求和期望保持一致。
想象你在一个大型组织中实施新的数据驱动战略。如果数据团队仅在最终阶段呈现分析而没有事先的参与,利益相关者可能会发现难以掌握其细微之处或看出其与他们具体背景的相关性。然而,通过在定期的间隔中涉及这些利益相关者——通过周期性演示或研讨会——他们会对数据及其影响变得更加熟悉。他们可以提供宝贵的反馈,引导分析朝着对他们最相关的领域发展,从而确保最终成果不仅可理解,而且可以立即采取行动并满足他们的需求。
使观众能够应用这些见解
可操作性围绕着将这种深层理解转化为现实世界的应用或决策。这是关于确保观众能够有效利用这些见解来推动实际结果。真正的关键在于思考分析与现实影响之间的“最后一公里”,以及你如何帮助消除任何阻碍以采纳你的见解。
例如,如果你正在进行一个定义用户流失的项目——使你的研究更易于理解可能包括创建一个仪表板,让你的业务利益相关者清楚地了解你的结果具体是什么样的。
其他想法包括举办研讨会、开发互动可视化等——任何可以帮助团队迅速适应的方式。
总结一下——从一开始就让主题专家(SMEs)参与和与利益相关者保持持续沟通,显著提升了数据分析项目的可消化性。这种协作方法确保了研究不仅易于理解,而且直接相关且有价值,符合其预期受益者的需求。
总结
成功的数据分析是技术熟练、战略对齐和实际适用性的结合体。它不仅仅是遵循一套步骤,而是理解并根据每个项目独特的背景调整这些步骤。及时性、正确执行和解决实际的组织需求是支撑连接数据分析与组织成功的桥梁的支柱。最终目标是将数据转化为能驱动价值和指导战略决策的可行洞察。
希望您喜欢阅读这篇文章!您有什么想分享的小贴士吗?在评论区告诉大家吧!
PS:此文章已经发布到 Analytics Explained,这是一个新闻简报,我在其中总结了我在各种分析角色(从新加坡初创公司到旧金山的大科技公司)中学到的内容,并回答读者关于分析、增长和职业方面的问题。
《提示设计的艺术:提示边界与标记修复》
·
关注 发表在 Towards Data Science ·7 分钟阅读·2023 年 5 月 8 日
--
所有图像均为原创作品。
这篇文章(与 Marco Tulio Ribeiro 共同撰写)是关于提示设计艺术系列的第二部分(第一部分 这里),在其中我们讨论了如何通过 [guidance](https://github.com/microsoft/guidance)
控制大型语言模型(LLMs)。
在这篇文章中,我们将讨论语言模型使用的贪婪/优化标记化方法如何在提示中引入微妙而强大的偏差,从而导致令人困惑的生成结果。
语言模型不是在原始文本上进行训练,而是在标记(tokens)上进行训练,标记是经常一起出现的文本块,类似于单词。这会影响语言模型如何“看待”文本,包括提示(因为提示只是标记的集合)。GPT 风格的模型使用像 字节对编码(BPE)这样的标记化方法,它们以贪婪的方式将所有输入字节映射到标记 ID。这在训练时是可以接受的,但在推理过程中可能会导致微妙的问题,如下例所示。
提示边界问题的一个例子
考虑以下示例,我们正在尝试生成 HTTP URL 字符串:
import transformers
# we use StableLM as an example, but these issues impact all models to varying degrees
generator = transformers.pipeline('text-generation', model='stabilityai/stablelm-base-alpha-3b')
raw_gen('The link is <a href="http:') # helper func to call the generator
笔记本输出。
注意,LLM 生成的输出没有用明显的下一个字符(两个斜杠)完成 URL。它反而创建了一个中间带有空格的无效 URL 字符串。这令人惊讶,因为在 http:
之后 //
的补全是非常明显的。要理解为什么会发生这种情况,让我们改变我们的提示边界,使提示不包括冒号字符:
raw_gen('The link is <a href="http')
现在,语言模型生成了我们预期的有效 URL 字符串。要理解 :
的重要性,我们需要查看提示的标记化表示。下面是以冒号结尾的提示的标记化(不包含冒号的提示有相同的标记化,除了最后一个标记):
print_tokens(generator.tokenizer.encode('The link is <a href="http:'))
现在注意有效 URL 的标记化是什么样的,特别关注 http
后的标记 1358
:
print_tokens(generator.tokenizer.encode('The link is <a href="http://www.google.com/search?q'))
大多数 LLM(包括这个)使用贪婪的标记化方法,总是偏向于选择最长的标记,即 ://
在完整文本中(例如在训练中)总是优于 :
。
在训练中,URL 使用标记 1358(://
)进行编码,而我们的提示使 LLM 看到标记 27
(:
),这会通过人为地将 ://
拆分开来,从而影响完成。
实际上,模型可以非常确定看到标记 27
(:
)意味着接下来的内容极不可能是可以与冒号一起编码的“更长的标记”像 ://
,因为在模型的训练数据中,这些字符会与冒号一起编码(稍后我们将讨论的一个例外是训练期间的子词正则化)。看到一个标记意味着同时看到该标记的嵌入 以及 之后的内容没有被贪婪的标记化器压缩,这一点容易被忘记,但在提示边界中很重要。
让我们搜索模型词汇表中所有标记的字符串表示,看看哪些标记以冒号开头:
N = generator.tokenizer.vocab_size
tokens = generator.tokenizer.convert_ids_to_tokens(range(N))
print_tokens([i for i,t in enumerate(tokens) if t.startswith(":")])
请注意,有34种不同的令牌以冒号开头,因此以冒号结尾的提示可能不会生成这些34种令牌字符串中的任何一种。这种微妙而强大的偏差可能会带来各种意想不到的后果。 这适用于任何可能被扩展为更长单一令牌的字符串(不仅仅是 :
)。即使是我们以“http”结尾的“固定”提示也存在内建偏差,因为它向模型传达了“http”后面内容可能不是“s”(否则“http”不会被编码为单独的令牌):
print_tokens([i for i,t in enumerate(tokens) if t.startswith("http")])
以为这是只影响 URL 的深奥问题而不值得担忧,请记住,大多数令牌化器会根据令牌是否以空格、标点、引号等开头来处理令牌,因此以这些内容结尾的提示可能会导致错误的令牌边界,从而破坏内容:
# Accidentally adding a space, will lead to weird generation
raw_gen('I read a book about ')
# No space, works as expected
raw_gen('I read a book about')
另一个例子是“[”字符。考虑以下提示和补全:
raw_gen('An example ["like this"] and another example ')
![
为什么第二个字符串没有被引号括起来?因为通过以“[
”令牌结尾,我们告诉模型不生成与以下 27 个更长令牌匹配的补全(其中一个增加了引号字符,15640
):
# note the Ġ is converted to a space by the tokenizer
print_tokens([i for i,t in enumerate(tokens) if t.startswith("Ġ[")])
令牌边界偏差无处不在。上述 StableLM 模型的70%最常见令牌是较长可能令牌的前缀,因此在提示中的最后一个令牌时会造成令牌边界偏差。*
通过“令牌修复”修正无意的偏差
我们可以做些什么来避免这些无意的偏差?一种选择是始终以不能扩展为更长令牌的令牌结束提示(例如用于基于聊天的模型的角色标签),但这是一种严重限制。
相反,guidance
具有一个叫做“令牌修复”的功能,它会在提示末尾之前自动将生成过程备份一个令牌,然后约束生成的第一个令牌具有与提示最后一个令牌匹配的前缀。在我们的 URL 示例中,这将意味着移除 :
并强制生成的第一个令牌具有 :
前缀。令牌修复允许用户以任何他们希望的方式表达提示,而无需担心令牌边界。
例如,让我们重新运行一些上述 URL 示例,并开启令牌修复功能(对于 Transformer 模型,默认是开启的,因此我们去掉 token_healing=False
):
from guidance import models, gen
# load StableLM from huggingface
lm = models.Transformers("stabilityai/stablelm-base-alpha-3b", device=0)
# With token healing we generate valid URLs,
# even when the prompt ends with a colon:
lm + 'The link is <a href="http:' + gen(max_tokens=10)
# With token healing, we will sometimes generate https URLs,
# even when the prompt ends with "http":
[str(lm + 'The link is <a href="http' + gen(max_tokens=10, temperature=1)) for i in range(10)]
同样,我们无需担心额外的空格:
# Accidentally adding a space will not impact generation
lm + 'I read a book about ' + gen(max_tokens=5)
# This will generate the same text as above
lm + 'I read a book about' + gen(max_tokens=6)
现在即使提示以“[
”令牌结尾,我们也能获得引号括起来的字符串:
lm + 'An example ["like this"] and another example ' + gen(max_tokens=10)

来控制大型语言模型(LLMs)系列文章的第一篇,由Marco Tulio Ribeiro和我共同撰写。我们将从基础知识开始,逐步深入到更高级的主题。
在这篇文章中,我们将展示明确的语法如何使你能够向 LLM 传达你的意图,并确保输出易于解析(就像保证有效的 JSON)。为了清晰和可重复性,我们将从一个未微调的开源 Mistral 7B 模型开始。然后,我们将展示这些相同的想法如何应用于像 ChatGPT / GPT-4 这样的微调模型。下面的所有代码都可在笔记本中找到,如果你愿意,可以复现。
明确的语法有助于解析输出
使用明确语法的第一个也是最明显的好处是它使得解析 LLM 的输出更容易。即使 LLM 能够生成正确的输出,可能也难以程序化地从输出中提取所需的信息。例如,考虑以下 Guidance 提示(其中gen()
是一个guidance
命令,用于从 LLM 生成文本):
from guidance import models, gen
# we use Mistral, but any model will do
lm = models.LlamaCpp("path/mistral-7b-v0.1.Q8_0.gguf")
# run a guidance program (by appending to the model state)
lm + "Name common Linux operating system commands." + gen(max_tokens=50)
输出在笔记本中的显示方式。
虽然答案是可读的,但输出的格式是任意的(即我们事先不知道),因此难以程序化解析。例如,这里是类似提示的另一轮,其中输出格式非常不同:
lm + "Name common Mac operating system commands." + gen(max_tokens=50)
在提示中强制使用明确的语法可以帮助减少任意输出格式的问题。你可以通过几种方式来实现这一点:
1. 在标准提示中给 LLM 提供结构提示(甚至可以使用少量示例)。
2. 使用guidance
(或其他包)来强制特定的输出格式。
这些并不是互相排斥的。让我们看一下每种方法的示例。
传统的提示与结构提示
这是一个传统提示的示例,它使用结构提示来鼓励使用特定的输出格式。提示旨在生成一个易于解析的 5 项列表。请注意,与之前的提示相比,我们编写了这个提示,使得 LLM 承诺使用特定的明确语法(数字后跟引号字符串)。这使得生成后的输出更容易解析。
lm +'''\
What are the most common commands used in the Linux operating system?
Here are the 5 most common commands:
1\. "''' + gen(max_tokens=50)
请注意,LLM 正确地遵循了语法,但在生成 5 项之后没有停止。我们可以通过创建明确的停止标准来解决这个问题,例如,要求生成 6 项,当看到第六项的开始时停止(这样我们就能得到五项):
lm + '''\
What are the most common commands used in the Linux operating system?
Here are the 6 most common commands:
1\. "''' + gen(max_tokens=100, stop="\n6.")
使用指导程序强制语法
与使用提示不同,Guidance 程序强制特定的输出格式,插入属于结构的一部分的标记,而不是让 LLM 生成它们。
例如,这就是我们如何在需要强制编号列表作为格式时操作:
lm2 = lm + """\
What are the most common commands used in the Linux operating system?
Here are the 5 most common commands:
"""
for i in range(5):
lm2 += f'''{i+1}. "{gen('commands', list_append=True, stop='"')}"\n'''
在上面的提示中,lm2 = lm + …
命令将添加一个字符串到起始 lm
状态后产生的新模型状态保存到变量 lm2
中。然后 for
循环通过添加混合字符串和生成序列来迭代更新 lm2
。请注意,结构(包括数字和引号)不是由 LLM 生成的。
输出解析由 guidance
程序自动完成,所以我们不需要担心这个问题。在这种情况下,commands
变量将是生成的命令名称列表:
out["commands"]
强制有效的 JSON 语法: 使用 guidance
我们可以以绝对的信心创建任何我们想要的语法,确保生成的内容完全遵循我们指定的格式。这对于 JSON 等内容特别有用:
import guidance
# define a re-usable "guidance function" that we can use below
@guidance
def quoted_list(lm, name, n):
for i in range(n):
if i > 0:
lm += ", "
lm += '"' + gen(name, list_append=True, stop='"') + '"'
return lm
lm + f"""\
What are the most common commands used in the Linux operating system?
Here are the 5 most common commands in JSON format:
{{
"commands": [{quoted_list('commands', 5)}],
"my_favorite_command": "{gen('favorite_command', stop='"')}"
}}"""
Guidance 加速: guidance
程序的另一个好处是速度——增量生成实际上比一次性生成整个列表要快,因为 LLM 不需要为列表本身生成语法标记,只需生成实际的命令名称(当输出结构更丰富时,这种差异更为明显)。
如果你使用的模型端点不支持这种加速(例如 OpenAI 模型),那么许多增量的 API 调用会让你变慢,因此 guidance
使用一个单独的运行流(请参见下面我们演示聊天模型的详细信息)。
清晰的语法赋予用户更多控制权
陷入低多样性的困境是大型语言模型(LLMs)常见的失败模式,即使我们使用相对较高的温度也可能发生:
lm2 = lm + """\
What are the most common commands used in the Linux operating system?
"""
for i in range(10):
lm2 += f'''- "{gen('commands', list_append=True, stop='"', temperature=0.8)}"\n'''
在生成项目列表时,列表中的前面项目会影响未来的项目。这可能会导致生成内容中的无用偏差或趋势。解决这个问题的一种可能方法是请求并行完成(以便之前生成的命令不会影响下一条命令的生成):
lm2 = lm + '''\
What are the most common commands used in the Linux operating system?
- "'''
commands = []
for i in range(10):
lm_tmp = lm2 + gen('command', stop='"', temperature=0.8)
commands.append(lm_tmp["command"])
out["commands"]
我们仍然会有一些重复,但比之前少得多。此外,由于清晰的结构使我们可以轻松解析和操作输出,我们可以轻松地提取输出,去除重复项,并在程序的下一步中使用它们。
这是一个示例程序,它接受列出的命令,挑选其中一个,并对其进行进一步操作:
lm2 = lm + 'What are the most common commands used in the Linux operating system?\n'
# generate a bunch of command names
lm_tmp = lm2 + 'Here is a common command: "'
for i in range(10):
commands.append(lm_tmp.gen('command', stop='"', max_tokens=20, temperature=1.0)["command"])
# discuss them
for i,command in enumerate(set(commands)):
lm2 += f'{i+1}. "{command}"\n'
lm2 += f'''\
Perhaps the most useful command from that list is: "{gen('cool_command', stop='"')}", because {gen('cool_command_desc', max_tokens=100, stop=guidance.newline)}
On a scale of 1-10, it has a coolness factor of: {gen('coolness', regex="[0-9]+")}.'''
我们在上述程序中引入了一种导入控制方法:生成的 regex
参数。命令 gen('coolness', regex='[0–9]+')
使用正则表达式对输出强制执行某种语法(即强制输出匹配任意正则表达式)。在这种情况下,我们强制 coolness 分数为一个整数(注意一旦模型完成模式生成并开始生成其他内容,生成过程将停止)。
将清晰的语法与特定模型的结构(如聊天)结合
上述所有示例都使用了基础模型,而没有经过后续的微调。但如果你使用的模型有微调,重要的是将清晰的语法与已经调整到模型中的结构结合起来。
例如,聊天模型已经过微调,以期望在提示中包含多个“角色”标签。我们可以利用这些标签进一步增强程序/提示的结构。
以下示例将上述提示调整为适用于基于聊天的模型。guidance
有特殊的角色标签(如 user()
),这些标签允许你标记不同的角色,并自动将其转换为你使用的 LLM 所需的正确特殊令牌或 API 调用。这有助于使提示更易读,并使其在不同的聊天模型中更加通用。
from guidance import user, assistant, system
# load a chat model
chat_lm = models.llama_cpp.MistralChat("path/mistral-7b-instruct-v0.2.Q8_0.gguf")
with user():
lm2 = chat_lm + "What are the most common commands used in the Linux operating system?"
with assistant():
# generate a bunch of command names
lm_tmp = lm2 + 'Here are ten common command names:\n'
for i in range(10):
lm_tmp += f'{i+1}. "' + gen('commands', list_append=True, stop='"', max_tokens=20, temperature=0.7) + '"\n'
# discuss them
for i,command in enumerate(set(lm_tmp["commands"])):
lm2 += f'{i+1}. "{command}"\n'
lm2 += f'''Perhaps the most useful command from that list is: "{gen('cool_command', stop='"')}", because {gen('cool_command_desc', max_tokens=100, stop=guidance.newline)}
On a scale of 1-10, it has a coolness factor of: {gen('coolness', regex="[0-9]+")}.'''
以笔记本中显示的形式输出。
使用 API 限制模型
当我们控制生成时,我们可以在过程的任何步骤中引导输出。但一些模型端点(例如 OpenAI 的 ChatGPT)目前有更有限的 API,例如,我们不能控制每个 role
块内部发生的事情。
虽然这限制了用户的能力,但我们仍然可以使用语法提示的子集,并在角色块之外强制执行结构:
# open an OpenAI chat model
gpt35 = models.OpenAI("gpt-3.5-turbo")
with system():
lm += "You are an expert unix systems admin that is willing follow any instructions."
with user():
lm += f"""\
What are the top ten most common commands used in the Linux operating system?
List the commands one per line. Please list them as 1\. "command" ...one per line with double quotes and no description."""
# generate a list of commands
with assistant():
lm_inner = lm
for i in range(10):
lm_inner += f'''{i+1}. "{gen('commands', list_append=True, stop='"', temperature=1)}"\n'''
# filter to make sure they are all unique then add them to the context (just as an example)
with assistant():
for i,command in enumerate(set(lm_inner["commands"])):
lm += f'{i+1}. "{command}"\n'
with user():
lm += "If you were to guess, which of the above commands would a sys admin think was the coolest? Just name the command, don't print anything else."
with assistant():
lm += gen('cool_command')
with user():
lm += "What is that command's coolness factor on a scale from 0-10? Just write the digit and nothing else."
with assistant():
lm += gen('coolness', regex="[0-9]+")
with user():
lm += "Why is that command so cool?"
with assistant():
lm += gen('cool_command_desc', max_tokens=100)
总结
每当你构建一个控制模型的提示时,重要的是要考虑提示的内容以及 syntax
。
清晰的语法使解析输出变得更容易,有助于 LLM 生成符合你意图的输出,并且让你能够编写复杂的多步骤程序。
即使是一个微不足道的示例(列出常见的操作系统命令)也会从清晰的语法中受益,更复杂的任务受益更多。我们希望这篇文章能给你一些如何利用清晰的语法来改进提示的想法。
同时,请务必查看一下 [guidance](https://github.com/guidance-ai/guidance)
。虽然你不一定需要它来编写具有清晰语法的提示,但我们认为它可以让你做到这一点变得容易得多。
解决任何数据科学问题的艺术 — 提高结果的简单技巧
避免麻烦并增加成功机会的未言明建议
·发表于 Towards Data Science ·阅读时间 9 分钟·2023 年 1 月 25 日
--
图片由 Olav Ahrens Røtne 提供,来源于 Unsplash
对数据科学感兴趣的大多数人学习解决数据科学问题的工具和技术。这些工具和技术在构建解决方案时绝对必要。但请记住,这仅仅是不够的。要提出高效的解决方案,人们需要学习问题解决的艺术。有很多课程教授数据科学中的工具和技术,但很少有课程教如何解决数据科学问题。
在这篇文章中,我的目标是通过实际案例帮助你理解解决数据科学问题的关键方面。我们还将看到这些如何帮助识别和解决核心业务问题,以及如何避免导致问题的常见陷阱。
为了确保这里讨论的概念更易于理解,我将以电信领域的客户流失为例。
问题概念化
图片由 Austin Distel 提供,来源于 Unsplash
首要目标应明确问题。诸如客户流失等问题可能非常棘手。一般而言,当客户停止使用产品或服务时,他们会被认为是流失。在电信行业,我们可以简单地将转向竞争对手或终止服务的客户视为流失。但有些情况可能不容易定义客户流失,例如在电子商务中。不同的电子商务客户可能会以不同的频率使用平台。因此,在这种情况下,需要进行广泛的数据分析以准确定义客户流失。另一个例子是约会网站,如何区分在平台上找到约会的人和因糟糕体验而退出的人。
现在回到电信行业的客户流失例子。应该花时间更好地理解客户行为。对客户有良好的理解将有助于更好地概念化问题,从而开发更好的解决方案。以下是一些有助于更好理解客户流失的问题。
-
客户的平均任期是多少?
-
客户的平均收入是多少?
-
客户的平均生命周期价值是多少?
-
获得新客户的成本是多少?
-
每月新增客户数量是多少?
-
客户支持部门报告的前 10 个投诉是什么?
-
哪些事件可能导致客户流失?
-
流失的客户去哪里/可能去哪里?
-
竞争的评判标准是什么?
-
根据内部团队的反馈,客户流失的原因是什么?通常,这些原因不会一致。
这些问题有助于清晰地概念化问题并专注于实现目标的解决方案。稍后在本文中,我将讨论可以用来更好理解问题的思维模型。
-
我们是否应该关注导致客户流失的事件,从而防止这些事件发生?
-
我们是否应该预测客户可能流失并防止他们流失?
-
我们是否只应专注于获得更多的新客户?
虽然在问题概念化中以数据驱动是好的,但以目的驱动更为重要。以数据驱动,我们在关键决策中利用数据,但通过目的驱动,还考虑了既定目标和目的。以目的驱动确保解决方案与组织的整体目标一致。
对数据环境的良好理解和灵活应对
在现实情况下,数据科学团队并不会提供解决问题所需的数据。数据科学家的主要职责之一是收集解决问题所需的所有数据。在大多数组织中,数据分布在来自不同平台的多个来源中。
在解决数据科学问题时,数据科学家通常会提出几个假设。在客户流失的情况下,一些相关的假设包括:
-
客户是否因为客服团队处理问题不当而离开?
-
客户是否因为竞争对手提供更好的优惠而离开?
-
客户是否因为服务差而离开?
-
客户是否因为技术问题而离开?
-
年轻专业人士在首次未解决的投诉后是否更容易流失?
-
长期客户支付更高价格是否更容易流失?
基于上述一些假设,分析中需要考虑的相关数据集包括:
-
客户档案数据
-
客户使用和账单数据
-
投诉数据和其他客户互动数据
-
关于技术问题的数据
-
关于竞争对手及其优惠的数据
更好地理解数据环境将有助于,
-
容易识别所有相关的数据集以验证假设
-
整合不同来源的数据集,并理解其局限性
-
了解数据质量问题,从而更好地准备应对这些问题
-
确保按照合规要求以安全和道德的方式处理个人身份信息
-
对数据集有良好的理解有助于数据探索。
避免分析中的偏见
对于我们正在解决的问题有看法是完全正常的。但是应注意确保这不会影响我们正在构建的解决方案或执行的分析。偏见不仅可能对项目产生很大影响,还可能对组织及其声誉造成负面影响。以下是偏见应得到妥善处理的主要原因:
-
这会导致不准确的结果,从而做出错误决策,进而对业务产生不良结果
-
这可能导致歧视性结果,使一部分人群处于不利地位
-
这可能导致对组织缺乏信任
为了更好地解释这个问题,我们以医学科学领域为例。如果用于诊断和治疗决策的模型因为训练数据不足而对某个群体或种族存在偏见,这可能会导致不准确的诊断和错误的治疗决策。这是选择偏差的经典例子。
同样地,假设一个政府机构或组织利用历史犯罪数据来识别可能重犯的人。历史数据中的偏见可能导致对某些群体的歧视和不公正。因此,在解决数据科学问题时,考虑偏见总是非常重要的。
让我们回到客户流失的例子。如果我们训练模型的数据没有代表某些群体,可能会导致结果偏斜。以下是避免分析中偏见的一些建议:
-
使用一个准确代表数据分析人群的样本
-
确保所有相关数据都被考虑用于数据分析
-
在独立数据集上测试模型以衡量实际性能
-
拥有多样化的团队并让他们参与关键项目相关决策
-
在解释结果时,总是寻找其他解释。
-
探索数据集中可能显示潜在偏见迹象的模式
-
清楚地记录过程、结果和解释,以便第三方可以审查是否存在偏见
头脑风暴
图片由 Jason Goodman 提供,来源于 Unsplash
头脑风暴不仅有助于避免偏见,而且在数据科学项目的成功中也非常有帮助。头脑风暴有助于产生广泛的想法,这对提出创新解决方案至关重要。以下是头脑风暴的一些好处,
-
它有助于提出独创性的方法来解决问题
-
这有助于促进团队成员之间更好的合作
-
它引入了不同的观点,以提出全面的解决方案
-
这是许多创新解决方案的关键原因之一
头脑风暴会议不仅仅用于内部团队讨论。为了获得更好的结果,这些会议应该是跨职能的。在处理客户流失问题时,所有直接或间接受到影响的相关方都应参与相关会议。例如,问题概念化应包括与以下团队进行头脑风暴。
-
客户支持团队:他们了解一些客户面临的关键问题
-
技术团队:他们了解新发布的功能和不同平台上的问题。
-
营销团队:他们对竞争中的活动和优惠有更好的了解。
使用心理模型来促进结构化思维
心理模型是一组工具,提供了理解和理清复杂信息的指导。在数据科学中,心理模型有助于更好地理解问题并简化问题解决过程。
让我们看看心理模型如何帮助解决客户流失问题。以下是一篇文章,解释了如何使用第一性原理来解决客户流失。
towardsdatascience.com
下面是心理模型在数据科学项目各个阶段中的作用。
-
问题定义:解决数据科学问题的第一步是理解问题。像第一性原理思维和费曼技巧这样的框架有助于更好地理解我们尝试解决的问题。
-
数据探索:探索性数据分析完全是关于提出相关问题并确保在解决问题时考虑到所有可能性。在这里可以采用的一些心理模型包括第一性原理思维、第二排序思维、自下而上和自上而下的方法,以及概率思维。
-
特征工程:心理模型有助于更好地理解领域和识别重要特征。在这里,有用的一些心理模型包括逆向思维、多重因果关系、根本原因和临近原因。
心理模型是帮助清晰思考的框架。我坚信心理模型可以用来解决任何类型的数据科学问题。我总是尽力利用思维框架来处理问题。如果你也觉得这很有趣并想进一步探索,这里有一篇关于利用心理模型提升数据科学职业生涯的文章。
激发超强思维的简单框架
解决任何数据科学问题时需要记住的事项
以下是你在处理问题时需要牢记的重要因素。这些因素可能与任何你从事的工作相关。
没有小工作
所有工作都非常重要,不应被轻视。许多数据科学项目涉及大量数据分析,但不涉及模型构建。对于数据科学新手来说,他们通常认为不需要模型构建的问题不够重要。理解这一点非常重要:业务关注的不是使用的模型或实现的复杂算法,而是它是否解决了问题。不要因为工作不涉及算法就轻视它!
这对于数据科学职业生涯的成功至关重要。人们总是会因为努力工作而被同行铭记。强烈的职业道德和愿意付出努力的态度在雇主眼中非常重要,它们总是会带来更好的职业机会。
永不妥协
另一个重要的事项是保持“永不满足”的态度。人们不应满足于简单的解决方案。你应该始终寻找机会来提升你的解决方案或方法。虽然这适用于任何职业,但在数据科学领域我发现它尤为重要。总会有交付解决方案的压力,但不应以此作为满足于简单解决方案的理由。有些选项可能需要更多努力,或者有时可能要求你学习新技能。保持开放的心态和永不满足的态度对长期职业成功帮助很大。
为了保持联系
-
如果你喜欢这篇文章并对类似内容感兴趣,关注我在 Medium 上的更新。成为Medium 会员,即可访问与职业、财富等相关的成千上万篇文章。
-
我在我的 YouTube 频道上教授和讨论各种数据科学主题。点击这里订阅我的频道。
-
点击这里注册我的电子邮件列表,获取更多数据科学技巧并与我的工作保持联系。
使用非线性编程优化您的营销预算
介绍 CVXPY,以最大化营销投资回报率
·发表于 Towards Data Science ·阅读时长 9 分钟·2023 年 5 月 22 日
--
图片由作者提供
在数字营销时代,企业面临着在多个渠道上分配营销预算以最大化销售的挑战。
然而,随着他们扩大覆盖范围,这些公司不可避免地面临 收益递减 的问题——这一现象指的是在营销渠道上额外投资会带来逐渐减少的转化增加。这正是营销预算分配概念发挥作用的地方,为整个过程增加了另一层复杂性。
在本文中,我们将探索非线性规划的潜力,特别是锥优化(或锥编程),作为营销预算分配的工具。通过使用这一高级数学技术,我们旨在优化在各个平台上的营销预算分配,以提取最大的价值和最高的投资回报率(ROI)。
代码可以在 GitHub 和 Google Colab 上找到。
💰 营销预算分配
营销预算分配是任何广告活动的关键方面,要求企业在不同渠道上战略性地分配资源。目标是最大化营销工作的效果,并实现最高的投资回报率(ROI)。为了解决这一挑战,我们需要考虑三个关键组件:
-
归因:我们如何将转化事件与特定活动关联?
-
绩效估算:我们如何根据分配的预算预测活动的绩效?
-
优化:我们如何在各种活动中分配预算以最大化 ROI?
🔗 1. 归因:将转换连接到活动
归因是确定哪些活动负责转化客户的过程。一些渠道,如 Facebook 或 AdWords,可以直接宣称转换。然而,有多种归因模型需要考虑,包括:
-
首次接触
-
最后接触
-
多次接触
-
时间衰减
-
基于位置
归因系统也并非没有问题,主要有两个挑战:
-
滞后:测量广告表现并准确归因转换所需的时间
-
归因窗口:使用短窗口与长窗口之间的权衡,以归因转换
例如,DoorDash 使用了几天的最后接触归因系统。他们面临的问题是需要等待几天才能测量广告的表现,这对于其市场的快速变化而言过于漫长。
🔮 2. 性能估算:预测活动成功
性能估算涉及创建一个模型,该模型可以根据预算分配预测营销活动的成功。在这里,成功可以通过各种关键绩效指标(KPI)来定义,例如:
-
潜在客户
-
每个潜在客户的成本(CPL)
-
客户终身价值(CLV)
-
客户获取成本(CAC)
传统上,线性模型用于性能估算。然而,它们假设营销渠道不会出现递减收益,这通常并非如此。为了获得非平凡的解决方案,线性模型通常包含多个约束,并通过线性规划(LP)求解。
实际上,市场营销组合建模中的响应曲线通常显示出不同的形状,例如:
-
线性(少见)
-
凹形(常见,表示递减收益)
-
凸形(少见)
-
S 形(少见)
图片由作者提供
这些形状反映了营销支出的递减收益或不同预算水平下不同渠道的效果差异。例如,将更多的钱投入到一个渠道可能会在初期产生更高的回报(凸形),但在某一点后,每增加一美元可能会产生越来越少的增量效果(变为凹形),最终形成 S 形曲线。
为了捕捉营销预算分配问题的内在非线性,需要更复杂的方法。这就是非线性规划,特别是锥形优化的作用所在。
🔄 3. 优化:使用 CVXPY 进行非线性优化
非线性规划,也称为非线性优化,是一种用于解决优化问题的方法,其中目标函数、约束条件或两者都是非线性的。简单来说,它是找到一个系统的最佳解决方案(最大化或最小化)的过程,该系统由一组非线性方程控制。
在这个例子中,我们将使用自然对数来建模每个营销渠道的回报(响应曲线),具体如下:
前两个步骤的归因和性能估计近似每个渠道的αᵢ和βᵢ值。让我们以三个渠道为例:
这些值中观察到的噪声在营销预算分配问题中是典型的。注意到α值是负的;这可以解释为与新的营销渠道接触的初始成本。
我们可以使用 matplotlib 绘制每个营销渠道的响应曲线。
import matplotlib.pyplot as plt
import numpy as np
np.random.seed(0)
TOTAL_BUDGET = 100_000
# Alpha and beta constants
alphas = np.array([-9453.72, -8312.84, -7371.33])
betas = np.array([8256.21, 7764.20, 7953.36])
# Linearly spaced numbers
x = np.linspace(1, TOTAL_BUDGET, TOTAL_BUDGET)
# Plot the response curves
fig = plt.figure(figsize=(10, 5), dpi=300)
plt.plot(x, alphas[0] + betas[0] * np.log(x), color='red', label='Google Ads')
plt.plot(x, alphas[1] + betas[1] * np.log(x), color='blue', label='Facebook Ads')
plt.plot(x, alphas[2] + betas[2] * np.log(x), color='green', label='Twitter Ads')
plt.xlabel('Budget ($)')
plt.ylabel('Returns ($)')
plt.legend()
plt.show()
如何找到每个响应曲线的最佳值?最简单的解决方案是贪心算法,它随机抽样值并评估结果。我们的优化问题可以描述如下:
以下函数有 1,000 次迭代的预算来寻找最佳分配。
def greedy_optimization(TOTAL_BUDGET, alphas, betas, num_iterations=1_000):
# Initialize the budget allocation and the best objective value
google_budget = facebook_budget = twitter_budget = TOTAL_BUDGET / 3
obj = alphas[0] + betas[0] * np.log(google_budget) + alphas[1] + betas[1] * np.log(facebook_budget) + alphas[2] + betas[2] * np.log(twitter_budget)
for _ in range(num_iterations):
# Generate a new random allocation
random_allocation = np.random.dirichlet(np.ones(3)) * TOTAL_BUDGET
google_budget_new, facebook_budget_new, twitter_budget_new = random_allocation
# Calculate the new objective value
new_obj = alphas[0] + betas[0] * np.log(google_budget_new) + alphas[1] + betas[1] * np.log(facebook_budget_new) + alphas[2] + betas[2] * np.log(twitter_budget_new)
# If the new allocation improves the objective value, keep it
if new_obj > obj:
google_budget, facebook_budget, twitter_budget = google_budget_new, facebook_budget_new, twitter_budget_new
obj = new_obj
# Return the best allocation and the corresponding objective value
return (google_budget, facebook_budget, twitter_budget), objp
让我们运行它并查看它找到的近似解:
# Run the greedy optimization
(best_google, best_facebook, best_twitter), obj = greedy_optimization(TOTAL_BUDGET, alphas, betas)
# Print the result
print('='*59 + '\n' + ' '*24 + 'Solution' + ' '*24 + '\n' + '='*59)
print(f'Returns = ${round(obj):,}\n')
print('Marketing allocation:')
print(f' - Google Ads = ${round(best_google):,}')
print(f' - Facebook Ads = ${round(best_facebook):,}')
print(f' - Twitter Ads = ${round(best_twitter):,}')
===========================================================
Solution
===========================================================
Returns = $224,534
Marketing allocation:
- Google Ads = $35,476
- Facebook Ads = $31,722
- Twitter Ads = $32,802
在进行计算后,我们发现我们的总回报是$224,533。你可能会想,我们是否可以通过进一步调整模型或进行更多迭代来改进它。
这种保证正是非线性编程的救援所在:它可以输出最佳可能解,也称为最优解。除了这一巨大优势外,它的运行速度也更快。
为了使用非线性编程解决营销预算分配问题,我们将使用CVXPY库,该库支持圆锥优化,得益于像ECOS、MOSEK(内点法)和SCS(一阶方法)等专门的求解器。在这个例子中,我们将使用开源的 ECOS 求解器来找到最佳解。
让我们设置优化问题:
-
我们的决策变量是每个渠道的(正)预算。
-
我们的约束条件是所有预算的总和不得超过总预算。
-
我们的目标是最大化总回报,即各个渠道回报的总和。
import cvxpy as cp
# Variables
google = cp.Variable(pos=True)
facebook = cp.Variable(pos=True)
twitter = cp.Variable(pos=True)
# Constraint
constraint = [google + facebook + twitter <= TOTAL_BUDGET]
# Objective
obj = cp.Maximize(alphas[0] + betas[0] * cp.log(google)
+ alphas[1] + betas[1] * cp.log(facebook)
+ alphas[2] + betas[2] * cp.log(twitter))
最后,我们调用 ECOS 求解器来找到最佳预算分配并显示结果。
# Solve
prob = cp.Problem(obj, constraint)
prob.solve(solver='ECOS', verbose=False)
# Print solution
print('='*59 + '\n' + ' '*24 + 'Solution' + ' '*24 + '\n' + '='*59)
print(f'Status = {prob.status}')
print(f'Returns = ${round(prob.value):,}\n')
print('Marketing allocation:')
print(f' - Google Ads = ${round(google.value):,}')
print(f' - Facebook Ads = ${round(facebook.value):,}')
print(f' - Twitter Ads = ${round(twitter.value):,}')
===========================================================
Solution
===========================================================
Status = optimal
Returns = $224,540
Marketing allocation:
- Google Ads = $34,439
- Facebook Ads = $32,386
- Twitter Ads = $33,175
求解器找到的最佳分配是 Google Ads $34,439,Facebook Ads $32,386,YouTube $33,175,总回报为$224,540!这比贪心算法返回的多$7($224,533)。
请记住,这种分配最大化了基于我们响应曲线的回报:正确建模这些曲线对于有效优化预算至关重要。
让我们将这个最佳分配可视化到之前的响应曲线之上。
# Plot the functions and the results
fig = plt.figure(figsize=(10, 5), dpi=300)
plt.plot(x, alphas[0] + betas[0] * np.log(x), color='red', label='Google Ads')
plt.plot(x, alphas[1] + betas[1] * np.log(x), color='blue', label='Facebook Ads')
plt.plot(x, alphas[2] + betas[2] * np.log(x), color='green', label='Twitter Ads')
# Plot optimal points
plt.scatter([google.value, facebook.value, twitter.value],
[alphas[0] + betas[0] * np.log(google.value),
alphas[1] + betas[1] * np.log(facebook.value),
alphas[2] + betas[2] * np.log(twitter.value)],
marker="+", color='black', zorder=10)
plt.xlabel('Budget ($)')
plt.ylabel('Returns ($)')
plt.legend()
plt.show()
但它真的最优吗?我们可以通过对贪心算法进行不同迭代次数的快速检验来检查。这将展示这两种方法之间的差异。
让我们对 1 到 1,000,000 之间的 20 个不同的迭代次数进行测试。
# List to store the best objective value for each number of iterations
best_obj_list = []
# Range of number of iterations to test
num_iterations_range = np.logspace(0, 6, 20).astype(int)
# Run the greedy algorithm for each number of iterations and store the best objective value
for num_iterations in num_iterations_range:
_, best_obj = greedy_optimization(TOTAL_BUDGET, alphas, betas, num_iterations)
best_obj_list.append(best_obj)
现在我们可以使用 matplotlib 绘制结果列表,并将其与最优解进行比较:
# Plot the results
plt.figure(figsize=(10, 5), dpi=300)
plt.ticklabel_format(useOffset=False)
plt.plot(num_iterations_range, best_obj_list, label='Greedy algorithm')
plt.axhline(y=prob.value, color='r', linestyle='--', label='Optimal solution (CVXPY)')
plt.xlabel('Number of iterations')
plt.xticks(num_iterations_range)
plt.xscale("log")
plt.ylabel('Best returns ($)')
plt.title('Best returns found by the greedy algorithm for different numbers of iterations')
plt.legend()
plt.show()
我们观察到,贪心算法在进行大量迭代时表现相对较好。然而,尽管进行了 100 万次尝试,它仍未能找到最优分配,最优回报为 224,540.1500 美元。它能达到的最佳非四舍五入值为 224,540.1489 美元。
除此之外,两种方法在计算速度方面存在显著差异。非线性编程模型在短短 22.3 毫秒内找到了最优解。而贪心算法则花费了 30 秒进行 100 万次迭代,找到一个几乎最优的解。
当我们将问题扩展到多个营销渠道时,这种差异变得尤为重要。使用 CVXPY 的非线性编程保持了其速度和精确度,使其成为处理复杂、高维营销预算分配问题的高效工具。
结论
非线性编程为解决营销预算分配问题提供了一种强大的方法。通过使用非线性函数来建模每个营销渠道的收益递减,并利用 CVXPY 库,我们可以找到最大化销售的资源最优分配。
随着营销环境的不断发展和渠道数量的增加,像非线性编程这样的优化技术可以帮助企业做出更好的数据驱动决策,优化其营销投资。虽然本文提供了一个起点,但还有许多更先进的技术和模型可以探索。继续学习和尝试,找到最适合你业务的方法。
如果你想了解更多,欢迎在 Twitter 上关注我 @maximelabonne。祝优化愉快!
参考文献
如果你想了解更多关于营销预算分配的信息,我推荐以下资源:
-
Park 等人,多个数字媒体渠道的广告预算分配的非线性优化模型(2022 年):基于收益递减的优秀方法,启发了本文。
-
Zhao 等人,营销预算分配的统一框架(2019 年):基于对数响应曲线的引人入胜的架构,目前在阿里巴巴生产中使用。
-
Katsov, 使用深度学习进行跨渠道营销支出优化(2019 年):关于一种有趣的基于 LSTM 的方法的博客文章,无需凸优化。
相关文章
使用 Google OR-Tools 的数学优化指南
towardsdatascience.com ## Python 中的整数规划与线性规划
识别和解决任何优化问题的指南
towardsdatascience.com
《有志统计学家的随机变量入门》
原文:
towardsdatascience.com/the-aspiring-statisticans-introduction-to-random-variables-7b26a057a89a
当宇宙大喊“惊喜!”时,它从不厌倦。
·发表于 Towards Data Science ·31 分钟阅读·2023 年 5 月 12 日
--
在这篇文章中,我们将踏上一次沉思的旅程,穿越机遇之地。我们将学习关于随机游走、离散和连续随机变量以及它们的概率分布。我们将理解为何给连续随机变量的值分配概率是没有意义的。在这个过程中,我们将揭示概率密度的含义。我们将学习随机过程的形成及其与时间序列模型的关系。最后,我们将通过简短的讨论结束我们的旅程,探讨随机变量如何构成所有回归模型的基础。
关于实验、结果和样本空间
我们经历变化——不可预测的、随机的变化,没有任何可观察的模式或理由。它让我们感到惊讶,令人沮丧,打乱我们最精心策划的计划。
我们在早晨的闹钟声中睡过头,我们被困在交通拥堵中,我们迟到了会议。结果发现会议被推迟,因为召集会议的人也被困在了交通中。
但随机性并不总是恶魔。还记得当分心的咖啡师多倒了几毫米咖啡的时候吗?你抗议了吗?我们已经学会了安静地享受大自然的小小意外。
我们追求可预测性、确定性和稳定性。但这些都是无法找到的。我们的感官和大脑并没有进化到能随时知道一切。我们经历随机变异,因为我们并非全知全能。另一方面,上帝可能用不到骰子。但即便如此,这也是有争议的。
我们学会了利用随机性。游戏中有赢有输是通过掷硬币决定的。一枚硬币的翻转决定了一匹珍贵马匹的归属。而正是一次硬币翻转给了俄勒冈州的波特兰市这个名字。
从更宏大的尺度来看,随机数驱动着加密大部分互联网数据的TLS 协议。
从更宏大的尺度来看,我们的太阳每大约 11 年左右就会翻转一次磁场。这里的“左右”意味着太阳不想准确告诉我们下一次翻转的时间。
你起床的时间、早晨杯中的咖啡量、你喝下它的次数、你一生中掷出的所有硬币的结果以及我们太阳翻转磁场所需的年数——这些都是随机数。随机值是通过某些活动(如掷硬币)产生的。在统计学中,这种活动被称为实验。
当你进行实验时,你期望一个结果,通常是几种可能结果中的一种。当你掷一枚硬币时,你期望它落在正面或反面。但是,如果它卡在泥土边缘或者滚进了排水沟呢?如果你在珠穆朗玛峰上掷硬币,风可能会把硬币吹走。如果你在国际空间站内掷硬币……嗯,你明白了。你是否应该忽略这些结果?这取决于你为什么要掷硬币。如果你的行动方案取决于硬币落在正面还是反面,你不关心其他任何结果,你会希望设计实验以确保得到正面或反面。需要记住的一点是,实验的设计决定了结果。作为实验者,你必须设计实验以只产生对你有意义的结果。
我们只对正面和反面感兴趣。因此,让我们在一个空房间的平整地面上掷硬币。地板上没有黏稠的泥土,也没有排水沟或格栅。这个受控实验的可能结果是{正面,反面}。这是实验的样本空间。它是所有对你有意义的可能结果的集合,用字母S表示。如果你观察到除了正面或反面以外的任何结果,你必须将其视为未发生过,并重新进行实验。你也可以重新设计实验,将意外结果纳入样本空间。或者,你可以简单地宣布失败然后去钓鱼。
如果实验产生了 N 种可能结果中的一种,那么样本空间S是{s_1, s_2, s_3,…,s_N}。S的大小,用|S|表示,是 N。
当你进行实验时,S中的每个结果都是可能的。当S={正面,反面}时,两种结果的发生概率分别记为 P(正面)和 P(反面)。在一般情况下,第 k 个结果的发生概率记为 P(s_k)。
在我们的受控实验中,硬币必须落地为正面或反面。没有第三种结果。这两种结果也是互斥的。如果硬币已经是正面,它不可能同时是反面。
所以硬币落地为“正面或反面”的概率是:
P(正面 ∪ 反面) = P(正面) + P(反面) — P(正面 ∩ 反面) = P(正面) + P(反面) = 1.0
这就像是在说,当你抛掷一枚硬币时,它落地的结果要么是正面,要么是反面,概率是 100%。在精心安排的实验的受控环境中,这种说法从不为假。逻辑学家称之为重言式。这可能听起来是个琐碎的声明,但对实验者来说非常实用。在你设计了只产生互斥结果的实验之后,你应该验证样本空间中所有结果的概率总和是否为 1.0。如果不是,那么你的实验设计存在问题。简单来说,你需要重新设计实验。
样本空间中所有结果的概率总和是完美的 1.0(即 100%)(图像来源:作者)
采取行动:在岛上随机漫步
之前我们谈到过根据实验结果采取一些行动。让我们进一步探讨。如果你是曼哈顿的游客,想要进行自助游,一般来说,可以通过拿一本旅游指南书或下载一个应用程序并按照其指示行事来实现。
但是如果你让硬币引导你在曼哈顿游览呢?下面是一个假设的“硬币操作”探索的运作方式:
假设我们从 W 34th St. & 8th Ave 的角落开始,正好在 Penn Station 外面。然后我们抛一枚硬币——也许不是一枚真正的硬币,而是一个由掷币应用程序生成的伪硬币。如果结果是正面,我们向右转,否则向左转。不管怎样,我们一直沿街走,直到遇到下一个 3 叉路口或 4 叉路口,然后再次抛硬币并重复这个过程。如果我们遇到死胡同,我们只需回到之前的交叉口。我们所经过的路径被称为随机漫步。
在曼哈顿的随机漫步。左转标记为蓝色。右转标记为红色。(图像来源:作者)(地图底图版权 OpenStreetMap 依据 OpenStreetMap 许可证)
在随机游走过程中,你会根据硬币是否显示正面或反面来选择右转或左转。我们将右转或左转的动作称为一个 事件。让我们创建一个新集合 E,其中包含这些 事件。E = {右转,左转}。
注意,右转或左转的动作完全是随机的。我们将用一个 随机变量 来表示这个动作,我们用加粗的大写字母 X 来表示。
换句话说,我们定义随机变量 X 来包含我们在硬币翻转后所采取的动作。因此,X 将根据 S={正面,反面} 中的结果假设 E={右转,左转} 中的两个值之一。注意 X 是如何将 S 中的值映射到 E 中的。结果到事件。
正面 → 右转
反面 → 左转
这样的映射让我们想起了什么?它们让我们想起了一个函数。X 是一个函数!所以让我们将其写成函数形式如下:
X(正面)= 右转
X(反面)= 左转
你也可以用以下集合符号写 X,它有额外的好处,让你看起来非常聪明:
X:S → E
无论如何,所有这些都表示 X 是一个函数,它将集合 S 中的值映射到集合 E 中的值。
随机变量 X 将结果映射到事件(作者提供的图像)
让我们回顾一下:随机变量是一个函数,它将随机结果集合 S 中的值映射到你感兴趣的事件集合 E 中。由于 X 是一个函数,它有一个定义域和一个值域。X 的定义域是样本空间 S 中的结果。X 的值域是 E 中的事件。
随机变量有三种类型:离散,连续,和 混合。X 恰好是第一种。因此,让我们仔细看看离散类别。
离散随机变量及其属性
随机变量 X 是驱动我们在曼哈顿随机游走的引擎。我们应当注意到 X 的以下属性:
-
X 假设为离散值“右转”和“左转”(与温度这样的连续值相对)。因此,X 是一个 离散随机变量。
-
你不能说右转比左转更大或更小。你不能对 X 取的两个值施加任何顺序。相反,你必须将它们视为相等。我们称这类随机变量,其范围包含无法排序的值为 名义随机变量。
-
由于每个结果 s_k 在 S 中都有一个关联的概率 P(s_k),每个事件 x_i 在 E 中也都有一个关联的概率 P(X=x_i)。
-
P(X=x_i) 被称为 概率质量函数 (PMF) 。PMF 为随机变量的每个可能值分配一个概率,即事件集合 E 中的每个值。
-
所有E中的 x_i 的概率总和为 1.0。这里没有惊喜。根据随机游走实验的设计,一旦你的硬币落在正面或反面,你将要么左转要么右转。E中没有其他可以随意选择的动作或事件。硬币完全和绝对地指引你的行动。你对这个问题没有个人意见,没有自由意志。但如果你想要自由意志怎么办?我们很快会讨论这个有趣的情况。现在,没有自由意志意味着 P(X=左转) + P(X=右转) = 1.0。
随机变量作为‘满射’函数
在随机游走实验中,X恰好将S中的每个值映射到E中的唯一值。正面映射到右转,反面映射到左转。集合论家称这样的函数为一一对应和满射。一一对应函数允许你将E中的某个值完全不映射到S中的任何东西。但你必须将它映射到S中的恰好一个值。这就是一一对应函数的工作原理。
一个满射函数要求你将E中的每一个值映射到S中的至少一个值。即使E中的多个值映射到S中的同一个值也无所谓。满射性要求的是E中的所有值都必须被映射。
结合一一对应和满射的属性意味着S中的每一个值都被映射到E中的恰好一个值,反之亦然。S和E最终具有相同数量的值。这是X的一个特征,但它不必是所有随机变量的特征。
随机变量不需要是一一对应的函数。但随机变量需要是满射函数。如果一个随机变量不是满射函数,会怎样呢?在这种情况下,它的范围E将包含一些与其定义域S中的任何结果都没有关联的事件。假设你在处理一个X,它的定义域S是{正面, 反面},范围E是{右转, 左转, 走进迎面而来的交通}。如果你的行动总是由一个硬币抛掷的结果来指导,告诉你在交叉路口左转或右转,那么你什么时候会故意走进迎面而来的交通呢?显然,永远不会。
现在,如果你在想,“为什么我的游走总是要听从一个愚蠢的硬币呢?难道我没有权利行使我的自由意志吗?”是的,你有。你刚刚要求的是另一个随机变量。为了帮助你行使自由意志,我们将定义一个新的变量Y,其定义域S是{正面, 反面, 练习自由意志},范围E是{右转, 左转, 做些事情(不是愚蠢的)}。
Y(正面)= 右转
Y(反面)= 左转
Y(练习自由意志)= 做些事情(不是愚蠢的)
再次强调,E中的所有内容都被映射到S中的某些东西,使得Y成为一个满射函数。当你设计随机变量时,你实际上不能避免将它们设计成满射函数。
一个更复杂的例子:右转次数计数
如前所述,随机变量不一定是一对一的函数。实际上,许多随机变量是多对一的函数。让我们看一个例子。
在你在曼哈顿的硬币控制的漫步中,如果你想跟踪你在任何 4 次转弯序列中右转的次数,你可以定义一个随机变量W来保存这个值。在你进行的任何 4 次转弯序列中,W将包含你右转的次数。W的值域是E={0, 1, 2, 3, 4},而W的定义域是一个包含所有可能的长度为 4 的正面和反面的序列的样本空间S。
S = {“HHHH”,“HHHT”,“HHTH”,“HHTT”,“HTHH”,“HTHT”,“HTTH”,“HTTT”,“THHH”,“THHT”,“THTH”,“THTT”,“TTHH”,“TTHT”,“TTTH”,“TTTT”}
在S中,我们也可以用右转替换 H,用左转替换 T。如果这样做,S的性质不会改变,但我们将保留 H 和 T,以便记住是硬币投掷驱动了W。
如前所述,W将S中的值映射到E中的值,但这次映射是多对一的:
随机变量W是一个多对一的映射函数(图片来源:作者)
随机变量W与X共享所有特性,除了一个。让我们回顾一下这些特性:
-
W是一个离散随机变量。
-
每个E={0, 1, 2, 3, 4}中的 w_k 都有一个概率 P(w_k)。
-
在W的概率质量函数中的所有概率 P(W=w_k)必须总和为 1.0。如果不是这样,那么W的定义可能存在问题。你应该重新检查一下它的定义。
-
现在,这里是X和W之间的区别:与X不同,W的值E={0, 1, 2, 3, 4} 可以被排序。你右转 0 次小于你右转 1 次,小于右转 2 次,依此类推。这使得W成为一个序数随机变量。回忆一下,X是一个名义随机变量。
概率质量函数
让我们讨论一下随机变量每个值的概率。我们将构建随机变量的概率质量函数。我们从X开始。回忆一下,X的定义域是样本空间S={正面,反面}。X的值域是事件空间E={右转,左转}。右转映射到{正面}在{正面,反面}中的子集。左转映射到子集{反面}。因此,{右转}发生的概率是{正面}子集的大小与样本空间S的大小之比。{正面}的大小表示为|{正面}|,显然为 1,而{正面,反面}的大小表示为|{正面,反面}| = |S|,为 2。因此,我们有:
P(X=右转) = |{正面}| / |S| = 1 / 2 = 0.5
类似地,P(X=左转) = |{反面}| / |S| = 1 / 2 = 0.5
X的 PMF 中所有概率的总和等于 1.0。
X的 PMF(图像由作者提供)
W的 PMF 稍微有趣一些。回忆一下,W是在 4 次掷硬币的任何序列中右转的次数。W的范围是集合E={0,1,2,3,4}。我们将通过构建一个表来计算概率 P(W=w_k)。我们的表将有 4 列。第一列将包含E中的四个值,即{0,1,2,3,4}。第二列将包含映射到这些四个值的S中的结果。例如,如果W=1,你在 4 次掷硬币的任何序列中做出了一次右转。结果为I1 的S中产生了正好一次右转的结果是{HTTT, THTT, TTHT, TTTH}。第三列将包含I的大小,第四列将包含概率 P(W=w_k)。
这是表格:
与W值相关的概率表(图像由作者提供)
让我们验证一下,概率 P(W=w_k)对于 k=0,1,2,3,4 的总和是否为 1.0:
1/16 + 4/16 + 6/16 + 4/16 + 1/16 = 16/16 = 1.0
这是W的 PMF 图的样子:
W的 PMF(图像由作者提供)
X和W的构建很简单。它们的样本空间很小,范围也小。为了计算它们的 PMFs,我们将映射到随机变量每个值的S中的结果计数相加。然后,我们将每个和除以样本空间的大小,如下所示:
P(W=w_k) = |I_w_k|/|S|
让我们来看其中一个概率。在 4 次转弯中做出 2 次右转的概率是 6/16 = 0.375 或 37.5%。这个概率究竟是什么意思?它的实际生活解释是什么?这里有一种解释方法:
每年有数百万游客访问曼哈顿。到达曼哈顿后,假设他们都迫不及待地想要进行一次岛上的步行游览。每个人从城市的 3 路或 4 路交叉口的角落开始他们的游览。在那里,他们从口袋里拿出一个公平的硬币并抛掷。根据它是正面还是反面,他们在交叉口处向右或向左转弯,然后前往下一个交叉口,再次抛掷硬币。他们将这个过程重复四次。当他们都满足了自己的步行欲望后,一个全知的存在统计了所有包含恰好 2 次右转的游览次数,并将这个数字除以总游览次数(或人数)。得到的结果将是大约0.375。
累积分布函数
一旦你知道了W的 PMF,你还可以回答诸如在 4 次转弯的序列中,大多数转弯是否为右转?或者如果你不喜欢右转,最多只有 1 次右转?为了回答这些可能在某些情况下无关紧要但在一般情况下有用的问题,我们需要构建一个叫做累积分布函数(CDF)的新函数。W的 CDF 将返回W的值至多为 k的概率,其中 k=0,1,2,3 或 4。
如果你知道 PMF,构建 CDF 有一个简单的方法:
W=k 的 CDF 是 k=0,1,2,…,k 的概率之和。搞定!
CDF 用大写字母 F 表示。
W的累积分布函数(图像由作者提供)
让我们使用W的 PMF 来计算 CDF:
F(W=0) = P(W <= 0) = P(W=0) = 1/16
F(W=1) = P(W <= 1) = P(W=0) + P(W=1) = 1/16 + 4/16 = 5/16
F(W=2) = P(W <= 2) = P(W=0) + P(W=1) + P(W=2) = 1/16 + 4/16 + 6/16 = 11/16
F(W=3) = P(W <= 3) = P(W=0) + P(W=1) + P(W=2) + P(W=3) = 1/16 + 4/16 + 6/16 + 4/16 = 15/16
F(W=4) = P(W <= 4) = P(W=0) + P(W=1) + P(W=2) + P(W=3) + P(W=4) = 1/16 + 4/16 + 6/16 + 4/16 + 1/16 = 16/16 = 1.0
F(W=5) = P(W <= 5) = P(W=0) + P(W=1) + P(W=2) + P(W=3) + P(W=4) + P(W=5) = 1/16 + 4/16 + 6/16 + 4/16 + 1/16 + 0/16 = 16/16 = 1.0
这里是W的 CDF 的六个值:
F(W=0) = 1/16
F(W=1) = 5/16
F(W=2) = 11/16
F(W=3) = 15/16
F(W=4) = 16/16
F(W=5) 和 P(任何其他大于 4 的值) = 16/16
你可能还想知道 F(.)是否在W介于 0 和 1、1 和 2 等之间定义,即W的分数值。从某种意义上说,计算此类值的 CDF 是毫无意义的。1.5、2.6、3.1415926 等值不属于W的范围。但我们仍然可以计算F对于这些虚构的中间值。因此,根据定义,F(.)的定义域可以具有分数(实数)值,对应的概率也是有意义的。让我们计算 F(W=1.5):
F(W=1.5) = P(W <= 1.5) = P(W=0) + P(W=1) + P(1 < W <= 1.5) = 1/16 + 4/16 + 0 = 5/16
我们可以类似地计算 1, 2)区间内每一个无限个实数的 F(.)。对于每一个数值,CDF 将返回概率 5/16。因此,F(.)的图形将是一条从 k=1 到 k=2 的直线,之后它将跃升到 F(W=2)=11/16。
如果你为每个 k 绘制 F(W=k),你会发现如下的阶梯图:
![W的 CDF(图像由作者提供)使用 CDF,你可以计算在 4 次转弯的序列中采取大多数右转的概率,如下所示:P(k >2) = 1 — P(k <= 2) = 1 — F(W=2) = 1–11/16 = 5/16 在一个包含 4 次转动的序列中,最多取 1 次右转的概率是:P(k <= 1) = F(W=1)。同样是 5/16。看到用 CDF 计算这种概率有多简单了吗?对于大型样本空间,我们用于计算 PMF(然后计算 CDF)的表格方法显然不够实际。例如,如果 W 表示在 10 次硬币导向的左转和右转序列中右转的次数,你必须处理 S 中 2¹⁰=1024 种不同的结果。对于 20 次硬币导向的转动,你将处理超过一百万种结果。在处理大型复杂样本空间时,你必须运用适当的数学公式。例如,如果随机变量 Z 表示在 N 次硬币导向的左/右转中你向右转的次数,其中 N 可以是任意大的,Z 的定义域是一个包含 2^N 个独特 N 长度的左转和右转序列的样本空间。Z 的取值范围是 E={0,1,2,3,…,N}。S 中包含恰好 k 次右转的序列数由以下组合公式给出:
从 N 个结果中选择 k 个相似结果的方式数(作者提供的图片)
Z 取值 k 的概率是 I 的大小与 S 的大小之比:
在 N 次硬币导向的转动序列中取 k 次右转的概率(作者提供的图片)
对于每个 k=0,1,2,…N 的值,如果你绘制相应的概率 P(Z=k),你会得到一个钟形曲线,如果 N 是偶数,则峰值在 k=N/2 处;如果 N 是奇数,则峰值在 k=(N-1)/2 和 k=[(N-1)/2 + 1] 处。要理解为什么它在这些值处达到峰值,我们可以依据硬币的公平性来解释:
当你的硬币是公平的时,在 N 次翻转的序列中,正面朝上的预期次数是 N/2。因此,期望你向右转的次数也是 N/2。所以,你可以预期在所有其他右转次数的概率中, N/2 次右转的概率是最高的。这解释了 PMF 在接近 N/2 处的单峰。下面的图显示了当 N=60 时 Z 的 PMF。
Z 的 PMF(作者提供的图片)
如果你沿 Y 轴往上看,你会发现大多数Z值的概率是极其微小的。这主要是因为 P(Z=k)公式中的分母有阶乘。当 k 很小的时候,k!很小,但(N-k)!很大,加上 2^N,它们一起使得分母极其庞大。这使得 k 小值时的概率非常微小。当 k 很大时,k!极其庞大,这次它主导了分母,再次把大值 k 的概率压得很低。在 60 次试验中,如果你希望右转次数少于 20 次或多于 40 次,那是不可能的。
Z的 CDF 是 F(Z = k) = P(Z <= k) = P(Z=0) + P(Z=1) + … + P(Z=k)。
我们按如下方式计算这个总和:
Z的 CDF(图片由作者提供)
连续随机变量
现在我们来看看随机领域中的另一种生物:连续随机变量。为了理解它,我们将查看 205 辆汽车的轴距长度:
205 辆汽车的轴距(数据来源:UCI 机器学习数据集库,使用(CC BY 4.0)许可证)(图片由作者提供)
这看似是一个无聊的例子,但它完美地说明了连续随机变量的普遍性。
我们的样本空间是 205 辆汽车的集合。我们将给每辆车一个唯一的 ID,从 1 到 205。因此,S={1,2,3,…,205}。不要把这些标识符看作数字。每个 ID 仅仅是数据集中某辆特定车辆的代理。我们将在这个样本空间中定义一个随机变量Y,将每个车辆标识符S映射到其轴距(以英寸为单位)。问题是,Y的范围是什么?
要回答这个问题,我们必须看一下“距离”的本质。轴距是指车辆前后轴之间的距离。你测量它的准确度受限于你的测量工具的精度以及你可能应用的任何刻意舍入。对于一辆两厢本田车,你可能将轴距测量为 86.57754 英寸,然后舍入到 86.6 英寸。如果你将其舍入到三位小数,那将是 86.578 英寸,四位小数——86.5775 英寸,依此类推。如果有两辆车的轴距分别为 86.57754 和 86.57755 英寸,你可以设想有第三辆、第四辆,甚至任意数量的车,其轴距介于这两个数值之间。不论这两个数字多么接近,总会有一个数字在它们之间。关键是轴距是一个具有无限理论精度的实数。因此,Y的范围是正实数的集合。
这引出了描述连续随机变量的以下方法:
范围是实数集(记作ℝ)的随机变量,或ℝ内的任何连续区间被称为连续随机变量。
因此,Y是一个将S映射到ℝ的子集的函数。它是ℝ的哪个子集呢?由于我们在讨论距离,这个子集是所有正实数的集合,记作ℝ>0。我们将戴上“看起来聪明”的帽子,并用集合符号表示Y如下:
Y : S → ℝ>0
如果Y是离散随机变量,它的范围将是有限的。在这个范围上定义的概率质量函数会为范围内的每个值分配一个概率。你只需确保所有概率加起来等于 1.0。然而,Y是连续的,其范围是ℝ>0,这是一个不可数的无限集,它比“仅仅”无限集还要大。任何尝试在这个集合上构造概率质量函数的努力注定会失败。概率将永远加起来是无穷大!
但肯定的是,每个轴距都有一个概率测度。即使在我们 205 辆车的样本中,我们也看到一些轴距比其他轴距出现得更频繁。轴距的频率分布很容易说明这一点:
车轮轴距的频率分布(图像由作者提供)
假设你需要根据这个数据集猜测随机选择的车辆的轴距。在甚至不查看车辆的情况下,你会希望将其轴距猜测在包含最多轴距测量值的范围内。这个范围是 93.8 到 97.4 英寸,它包含 81 个测量值,所以你会对 81 / 205,即大约 40%的时间猜对。如果你想做出更精确的猜测,你可以将区间大小从 3.6 减少到 1.8 英寸,并根据修正后的频率分布进行猜测,如下所示:
车轮轴距的频率分布(图像由作者提供)
通过将区间大小缩小 50%,我们将区间(93.8, 97.4]分成两个区间(93.8, 95.6]和(95.6, 97.4]。你现在可以将你的猜测“上调”到(95.6, 97.4],并在 54/205,即大约 26%的时间内猜对。这些计算使我们相信,对于连续随机变量,概率至少在值的区间上是有意义的。让我们进一步探讨这个问题。
累积分布函数,重温
请记住,我们的 205 辆车的数据集只是从理论上(不可数)无限的汽车及其相应的实际车轮基数中抽取的小样本。在这个数据集中,如(93.8, 97.4]这样的区间中,理论上(不可数)存在无限多个车轮基数测量值。连续随机变量Y可以取这些值中的任何一个,而无法知道Y取某个特定值的概率。但我们可能找到了一种巧妙的解决办法。
只要我们避免单独的Y值,而是计算Y在某个值范围内的概率,我们就能得到一个相当不错的概率估计。这就是关于连续随机变量需要记住的关键——概率的估计仅在值的范围内才有意义。
一个有用的范围是(-∞, y]。随之而来的概率 P(Y <= y)再次引导我们到累积分布函数(CDF)中。回忆一下,离散随机变量的 CDF 是一个阶梯函数。这个阶梯函数在每个随机变量的值处是不连续的。但由于Y是一个连续随机变量,它的 CDF 不是阶梯函数。相反,它的 CDF 是Y的一个连续函数。那么这个 CDF 的形状可能是什么样的呢?猜测其形状的一种方法是将Y视为一个离散变量。也就是说,我们计算数据集中每个 y 值的 P(Y <= y)的概率。为了方便计算Y的 P(Y <= y)概率,我们将数据排序,使其看起来像这样:86.6, 86.6, 88.4, 88.6, 88.6, 89.5, …, 115.6, 115.6, 120.9。接下来,我们按照以下方式计算概率:
P(Y <= 86.6) = 2/205 = 0.0097561
P(Y <= 88.4) = 3/205 = 0.01463415
P(Y <= 86.6) = 5/205 = 0.02439024
…
…
…
P(Y <= 120.9) = 205/205 = 1.0
事实上,我们可以做得更好。我们将在Y的相邻值之间进行插值。例如,我们假设在 88.4 和 88.6 之间存在一个神秘的数据点 x,并将概率 P(Y <= x) = 4/205 = 0.0195122 分配给两个 86.6 值中的第一个。这种(有些笨拙的平滑技术)确实通过在曲线中添加额外的步骤来平滑 CDF。不管怎样,这就是我们劳动成果的样子:
Y的经验 CDF(图像来源于作者)
再次强调,Y是一个连续随机变量。Y的真实累积分布函数(CDF)是Y的一个连续函数,其形状可能看起来有点像我们构建的阶梯近似函数。不过,真实的 CDF 也可能与阶梯函数完全不同。这完全依赖于我们默默做出的一个关键假设。我们假设我们的 205 辆车的样本能很好地代表总体的特征。如果我们的样本未能做到这一点,那么关于 CDF 的真实形状的一切假设都不成立。
话虽如此,Y 的经验累积分布函数确实有点像正态分布变量的累积分布函数。那么我们来直观地测试一下这个猜测。在我们的数据样本中,平均车轴长度为 98.75659 英寸,标准差为 6.02176 英寸。在经验累积分布函数上,如果我们叠加一个均值为 98.75659、标准差为 6.02176 的正态分布连续随机变量的累积分布函数,看起来是这样的:
正态分布随机变量的累积分布函数叠加在Y的经验累积分布函数上(作者提供的图像)
视觉上,Y 的经验累积分布函数(CDF)似乎以某种程度上拟合了正态变量的累积分布函数。但在车轴长度谱的低区域和中间区域,这种拟合显然不是很好。如果我们使用拟合的正态累积分布函数来估计任何量,这种累积分布函数与数据的不良拟合将导致系统性偏差进入概率估计中。
那么我们要进行什么样的估计呢?一个有用的估计是车轴长度落在某个区间 (a, b] 内的概率。这只是 CDF 在区间两端返回的值之间的差异。
P(a < Y <= b) = P(Y <= b) — P(Y <= a) = F(Y=b) — F(Y=a)
让我们计算区间 (93.8, 97.4] 的概率。记得我们是如何通过频率分布图计算出这个概率为 40%的吗?让我们看看使用经验累积分布函数及其对正态累积分布函数曲线的近似,这个概率是多少。
我们将从设置 a 和 b 开始:
a = 93.8, b = 97.4
在下图中,我们用红色的括号标记了 CDF 曲线 X 轴上的区间 (93.8, 97.4]。我们还将在两个图上标出以下点,从 Y 轴读取累积分布函数值 F(Y=y):
在蓝色图上:
(a, F(Y=a)) = (93.8, F(Y=93.8)) = (93.8, 0.18315)
(b, F(Y=b)) = (97.4, F(Y=97.4)) = (97.4, 0.57982)
在橙色图上:
(a, F(Y=a)) = (93.8, F(Y=93.8)) = (93.8, 0.20522)
(b, F(Y=b)) = (97.4, F(Y=97.4)) = (97.4, 0.41088)
(作者提供的图像)
在任一图上,车轴长度落在区间 (93.8, 97.4] 的概率是:
P(93.8 < Y <= 97.4) = F(Y=97.4) — F(Y=93.8)
如果我们从蓝色经验 CDF 中提取 F(.) 的值,我们得到车轮基距在区间 (93.8, 97.4] 内的概率为 (0.57982–0.18315) = 0.39667(或约 40%)。这与我们使用频率直方图得到的 40%估计值相符。对于这些估计值一致不会感到惊讶。这两个估计值都是从相同的数据样本使用两种不同的方法计算得出的。但如果我们使用理想化的正态 CDF 计算 P(93.8 < Y <= 97.4),我们得到的值是 (0.41088–0.20522) = .20566(或约 21%)。这个值几乎是经验值的一半。如果你查看理想化 CDF 在曲线中间部分与经验版本的偏差,这种向下偏差是可以预期的。
概率密度函数
我们使用的区间大小是 3.6 英寸。你可以将它压缩到一个仍然大于零的任意小长度。CDF 仍然会给你一个有效的车轮基距位于这个超小区间内的概率估计。
例如,如果你想找到车轮基距在以 93.8 英寸为起点,长度为.000001 英寸的区间内的概率。所以你设置 y=93.8 和 δy = .000001,然后计算 F(Y=y+δy) — F(Y=y)。使用标准正态分布 CDF,你可以这样计算这个概率:
P(93.8 < Y <= 93.800001) = 0.20522269383945058–0.20522264662597367 = 4.721347690583855e-08
这是一个非常小的数字。但仍然是有限的正概率。如果你进一步压缩这个区间,与之相关的概率将会进一步减少。如果你想探索在 93.8 处的概率性质呢?为此,你必须切换到不同的度量。这个度量就是概率密度,它由函数 f(.)表示。计算时,你将区间内的发生概率除以区间的长度:
概率在有限区间内的密度(图片由作者提供)
但是如果概率在 (y, y + δy] 区间内不均匀分布怎么办?这难道不会降低Y=y 处密度估计的准确性吗?当我们将直方图图的箱体大小从 3.6 英寸缩小到 1.8 英寸时,正遇到了这种情况。当我们缩小箱体大小时,直方图的 X 轴上的每个区间都缩小了一半。区间 (93.8, 97.4] 被拆分成两个区间 (93.8, 95.6] 和 (95.6, 97.4]。在父区间 (93.8, 97.4] 内有 81 个车轮基距测量值。但这 81 个测量值并没有按大约 1:1 的比例均匀拆分到两个子区间中。相反,27 个测量值进入了 (93.8, 95.6],而剩余的 54 个进入了 (95.6, 97.4]。这揭示了父区间 (93.8, 97.4] 内的概率并不均匀。
如果我们假设在区间(93.8, 95.6]内概率是均匀分布的,我们可以使用经验 CDF 来计算概率密度在 93.8 英寸处的估计值。我们将δy 设置为 3.6 英寸,并按如下方式计算密度:
f(Y=93.8) = [ F(Y=93.8+3.6) — F(Y=93.8) ] / 3.6 = (0.57982–0.18315) / 3.6 = 0.11019
但我们知道在这个区间内概率并不是均匀的。因此,如果我们将δy 缩小到 1.8 英寸,我们预计 93.8 处的密度估计会发生变化。结果确实如此:
f(Y=93.8) = [ F(Y=93.8+1.8) — F(Y=93.8) ] / 1.8 = (0.31587–0.18315) / 1.8 = 0.07373
如果我们进一步将δy 缩小到 0.9 英寸,我们发现估计值再次发生变化:
f(Y=93.8) = [ F(Y=93.8+0.9) — F(Y=93.8) ] / 0.9 = (0.28948–0.18315) / 0.9 = 0.11814
如果我们继续缩小δy,Y=93.8 的概率密度估计值将不断变化。它可能会趋向于一个稳定值,或者可能会在没有明显模式的情况下波动。哪种行为取决于我们的样本数据集的“表现良好”程度——它是否能代表总体。无论密度估计是否波动或趋于收敛,它都不会是Y=93.8 的真实值。那么Y=93.8 处的真实概率密度值是什么呢?真实值,或者统计学家喜欢称之为渐近值,是当间隔δy 缩小到一个无限小的大小时。如果这让你所有的微积分爱好者眼睛发光,你不会失望的。Y=93.8 处的真实概率密度值由以下极限给出:
Y=93.8 英寸处的概率密度(图像作者提供)
从微积分中,我们知道上述极限是Y在 y = 93.8 处的 CDF 的导数。
一般而言,连续随机变量Y的概率密度是其 CDF 的导数:
Y=y 处的概率密度(图像作者提供)
要使用上述 PDF 公式,你需要知道连续随机变量的 CDF。对于Y,我们展示了如何用均值为 98.75659 和标准差为 6.02176 的正态分布随机变量来近似其经验 CDF。如果你对这个正态 CDF 进行微分,你会得到其 PDF,这就是我们熟悉的经典钟形曲线:
N(μ=98.75659, σ²=36.26159)分布的连续随机变量的 PDF(图像作者提供)
反之,如果你知道 PDF f(Y),你可以通过积分 f(Y)得到 CDF:
通过积分 Y 的 PDF 得到 Y 的 CDF(图像作者提供)
如果到现在你感到有些轻微的不安,你并不孤单。那么我们稍微停下来一下。我们一直在轻松地讨论导数和积分,同时随意地在Y(假设Y是离散变量)的经验累积分布函数(CDF)和它的一些连续近似(假设Y是连续变量)之间切换。因此,在这些隐含的假设未被打破之前,让我们先擦清楚眼镜。我们使用的经验 CDF 只是一个工作道具——一个帮助我们说明如何计算 CDF 以及如何从“差分”中获得 PDF 的辅助工具。经验 CDF 在每一步都是不连续的。因此,它实际上是不可导的。但在某些时候,我们必须勇敢地将尽可能好的曲线拟合到经验数据上,这条曲线是连续且可导的,其中的导数和积分是有意义的。我们第一次尝试拟合这样的曲线是使用正态分布变量的 CDF。我们发现它拟合得并不是很好。充其量,它是一个不错的一阶近似,我们可能能够找到其他更合适的曲线。
接下来怎么办?
除了我们所看到的基本概念外,还有很多关于随机变量的知识。例如,通过使用函数,你可以将不同的随机变量组合起来以创建新的随机变量。新变量的属性——其均值、方差、概率质量函数或概率密度函数、累积分布函数可能与单个变量的属性大相径庭。形成线性回归模型基础的一个组合是随机变量的线性组合,如W = aX + bY + cZ。线性组合具有一些特殊的性质。例如,W的均值只是‘a’乘以X的均值,加上‘b’乘以Y的均值,再加上‘c’乘以Z的均值。另一个有趣的组合是通过结合离散随机变量和连续随机变量得到的。这种新产生的变量称为混合随机变量。混合随机变量用于形成混合分布和混合模型。
实际上,回归模型的研究本质上是随机变量的研究。回归模型中的因变量——y变量——是由某种概率分布支配的随机变量。回归变量——X变量——也是随机变量,每个变量的波动由各自的分布决定。一个常见的X示例是虚拟回归变量——一个可以是 0 或 1 的变量。虚拟变量是伯努利随机变量,其概率质量函数如下:
伯努利分布随机变量的概率质量函数(图片由作者提供)
或者一般来说,概率质量函数如下:
伯努利分布随机变量的概率质量函数(图片来源:作者)
当你抛掷一个(有偏或无偏的)硬币时,硬币正面或反面的出现是伯努利随机变量的完美例子。
随机过程
在结束之前,让我们向随机过程打个快招呼。如果你排列几个随机变量,你得到的就是一个随机过程。这将我们带回到曼哈顿的硬币驱动的漫游。
曼哈顿的随机游走(图片来源:作者)(地图背景由OpenStreetMap提供)
随机游走是由一系列硬币抛掷产生的。我们可以将其视为一系列 N 个伯努利随机变量的结果。这使得随机游走成为一个伯努利过程。实际的步行因此是这一过程的具体实现。
所有时间序列数据都是随机过程。如果数据集中包含 N 个时间索引观测值 y_1 到 y_N,每个观测值 y_i 都是一个随机变量。例如,在随机游走中,每个 y_t 都是一个伯努利随机变量。作为时间序列的一部分,y_t 显然排成一个序列,使整个时间序列数据集成为一个伯努利过程的单一观测实例。
在介绍随机变量的部分我们先到此为止。将这篇文章视为你的基点,一个你可以从中开始探索其他随机变量、其属性和应用的地方。
我将留给你一张图片和一个随机变量的想法。图片是美丽的 M2 球状星团。这个想法是定义一个随机变量,其值为图片中任意 10 mm x 10 mm 区域内的星星数量。这个变量的概率密度函数可能是什么样的呢?
球状星团 M2(图片来源:ESA/Hubble,授权方式为CC BY 4.0)
引用和版权
数据集
汽车数据集下载自UC Irvine 机器学习库,使用创意共享署名 4.0 国际(CC BY 4.0)许可证。
图片
本文中的所有图片版权归Sachin Date所有,授权方式为CC-BY-NC-SA,除非图片下方提及了不同的来源和版权。
如果你喜欢这篇文章,请关注Sachin Date,获取有关回归、时间序列分析和预测的技巧、教程和编程建议。
AutoML 的困境
基础设施工程师的观点
·
关注 发表在Towards Data Science · 8 分钟阅读·2023 年 9 月 16 日
--
图片由Fabrizio Conti拍摄,发布在Unsplash
近年来,AutoML 一直是一个热门话题。尽管其炒作声势浩大,甚至有取代人类机器学习专家的雄心,但由于长期未见广泛应用,对 AutoML 的期望正在迅速下降,这严格遵循 Gartner 曲线。
AutoML 在 Gartner 曲线上的表现(作者提供的图像)
在此时,我们需要了解 AutoML 的现状,并找出未来的发展方向。我是一名开发了两个 AutoML 库的 软件工程师,AutoKeras 和 KerasTuner。在这篇文章中,我将帮助你回顾 AutoML 的概念以及阻碍 AutoML 广泛应用的缺失部分。
什么是 AutoML?
想象一下,一个机器学习知识有限的人面临一个实际的图像分类问题。他们可以清楚地定义问题并有训练数据可用。在这种情况下,AutoML 可以帮助构建一个训练好的机器学习模型。
从输入和输出的角度来看,AutoML 做了以下工作。
从输入和输出的角度来看 AutoML(作者图片)
它接受问题定义和训练数据,并输出一个准备部署的训练好的机器学习模型。例如,如果给定一个图像分类任务,它接受训练图像数据集作为输入,并输出一个训练好的图像分类模型。
AutoML 尝试自动化的步骤可能包括数据预处理、特征工程、模型选择、超参数调整、神经架构搜索、模型训练、在测试数据上的推断和数据后处理。
总而言之,自动化机器学习(AutoML)试图弥合各种新颖机器学习模型和训练技术与它们可以解决的实际问题之间的差距,提供端到端解决方案以自动化方式进行。
AutoML 是如何工作的?
对于给定的任务和数据集,AutoML 系统会高效地尝试一系列相关方法或模型,并为你挑选出最佳的模型。
你可以把它看作是一个包含以下步骤的 for 循环:
-
生成模型配置。
-
创建并训练具有配置的模型。
-
对验证数据评估模型。
-
从评估结果中学习以改进配置。
AutoML 系统中的智能代理生成配置,并通过学习评估结果逐步改进。
许多算法可以作为智能代理使用,例如贝叶斯优化或强化学习。然而,在智能代理的核心,它所做的主要是函数近似和函数最大化。让我们逐一了解它们。
-
函数近似。智能代理试图学习模型配置与模型性能之间的关系。用数学语言来说,它试图学习一个函数 y=f(x),其中x 是模型配置,y 是模型的性能。
-
函数最大化。智能代理的最终目标是找到具有最佳模型性能的模型配置。换句话说,我们想找到x,使f(x) 的值最大化,即argmax f(x)。
AutoML 的影响
正如你所想象的那样,如果 AutoML 被广泛采用,其影响将是巨大的。它可以显著提高机器学习从业者的生产力。他们不再需要花费大量时间来微调模型配置的细节。他们可能只需要仔细定义任务并手动约束搜索空间,以更快地获得结果。
AutoML 今天能做什么?
当前 AutoML 的应用相当有限,主要集中在以下两个方面。
-
快速尝试。一些机器学习工程师可能希望快速尝试他们的任务和数据集上的机器学习。他们可以将 AutoML 作为起点。如果他们取得了相对良好的结果,可以进一步手动开发机器学习解决方案。
-
机器学习教育。刚开始学习机器学习的学生可能会使用 AutoML 来了解机器学习能做什么。他们不需要触及机器学习解决方案的所有细节,而是获得对整个过程的快速概览。
AutoML 未来能做什么?
对于 AutoML 未来的期望远高于目前的能力。我们将其总结为三个主要目标如下。
-
对于机器学习专家: 提高数据科学家和机器学习工程师的生产力。
-
对于领域专家: 领域专家,如医疗医生或机械工程师,可以轻松地将 AutoML 应用于他们的问题。
-
对于生产工程师: 发现的解决方案可以轻松部署到生产环境中。
AutoML 的问题
我们已经了解了我们现在的位置以及 AutoML 的未来发展方向。问题是我们如何达到那里。我们将今天面临的问题总结为三类。当这些问题得到解决时,AutoML 将实现广泛应用。
问题 1:缺乏商业激励
建模是微不足道的,相比于开发一个可用的机器学习解决方案,这可能包括但不限于数据收集、清洗、验证、模型部署和监控。对于任何能够负担得起雇佣人员完成所有这些步骤的公司而言,雇佣机器学习专家进行建模的成本开销是微不足道的。当他们能够在不增加太多成本开销的情况下建立一支专家团队时,他们不会费心去尝试新的技术,如 AutoML。
所以,人们只会在其他所有步骤的成本降到最低时才开始使用 AutoML。也就是当雇佣人员进行建模的成本变得显著时。现在,让我们看看我们的路线图。
许多步骤可以自动化。 我们应该对随着云服务的发展,开发机器学习解决方案中的许多步骤能够被自动化持乐观态度,比如数据验证、监控和服务。然而,有一个关键步骤是永远无法自动化的,那就是数据标注。除非机器能够自我学习,否则人类将始终需要为机器准备数据。
数据标注可能会成为开发机器学习解决方案的主要成本。如果我们能减少数据标注的成本,那么他们将有商业动机使用 AutoML 来消除建模成本,这将是开发机器学习解决方案的唯一成本。
长期解决方案:不幸的是,减少数据标注成本的终极解决方案今天并不存在。我们将依赖未来在“小数据学习”方面的研究突破。一种可能的途径是投资于迁移学习。
然而,人们对迁移学习不感兴趣,因为在这个主题上很难发表论文。有关更多详细信息,你可以观看这个视频,为什么大多数机器学习研究是无用的。
短期解决方案:在短期内,我们可以使用小数据对预训练的大模型进行微调,这是一种简单的迁移学习和小数据学习的方法。
总而言之,由于大多数机器学习解决方案的开发步骤已经通过云服务自动化,而 AutoML 可以使用预训练模型从较小的数据集中学习,以减少数据标注成本,企业将有商业动机应用 AutoML 来降低其机器学习建模成本。
问题 2:缺乏可维护性
所有深度学习模型都不可靠。模型的行为有时是不可预测的。很难理解模型为何给出特定的输出。
工程师维护模型。如今,我们需要工程师在模型出现问题时进行诊断和修复。公司与工程师沟通,处理他们想要更改的深度学习模型的任何事宜。
AutoML 系统比工程师更难以交互。如今,你只能将其作为一种一次性的方法来创建深度学习模型,通过提前给 AutoML 系统提供一系列明确定义的数学目标。如果在实践中遇到问题,它将无法帮助你解决。
长期解决方案:我们需要在 HCI(人机交互)领域进行更多研究。我们需要一种更直观的方式来定义目标,以使 AutoML 创建的模型更可靠。我们还需要更好的方法来与 AutoML 系统互动,以便在不花费过多资源重新搜索所有不同模型的情况下,更新模型以满足新需求或修复任何问题。
短期解决方案:支持更多的目标类型,例如 FLOPS 和参数数量,以限制模型大小和推理时间,以及加权混淆矩阵以处理不平衡数据。当模型出现问题时,人们可以向 AutoML 系统添加相关目标,以生成新模型。
问题 3:缺乏基础设施支持
在开发 AutoML 系统时,我们发现一些深度学习框架中需要的功能现在还不存在。没有这些功能,AutoML 系统的能力是有限的。它们总结如下。
第一,具有灵活统一 API 的最先进模型。 为了构建一个有效的 AutoML 系统,我们需要一个大规模的最先进模型池来组装最终解决方案。模型池需要定期更新和良好维护。此外,调用模型的 API 需要高度灵活和统一,以便我们可以从 AutoML 系统中以编程方式调用它们。它们作为构建端到端 ML 解决方案的构件。
为了解决这个问题,我们开发了 KerasCV 和 KerasNLP,这两个基于 Keras 的领域特定库,分别用于计算机视觉和自然语言处理任务。它们将最先进的模型封装成简单、清晰且灵活的 API,满足 AutoML 系统的要求。
第二,模型的自动硬件分配。 AutoML 系统可能需要在多台机器上的多个 GPU 之间构建和训练大型模型。AutoML 系统应能够在任何给定的计算资源上运行,这要求它动态决定如何为给定硬件分配模型(模型并行性)或训练数据(数据并行性)。
令人惊讶且不幸的是,目前没有任何深度学习框架能够自动将模型分布到多个 GPU 上。你需要明确指定每个张量的 GPU 分配。当硬件环境发生变化时,例如 GPU 数量减少,你的模型代码可能会失效。
我尚未看到对此问题的明确解决方案。我们必须给深度学习框架一些时间来发展。总有一天,模型定义代码将与张量硬件位置代码相互独立。
第三,模型的部署简易性。 任何由 AutoML 系统生成的模型可能需要在部署前在云服务、终端设备等下游环境中进行部署。假设你仍然需要雇佣工程师在部署之前针对特定硬件重新实现模型,而这在今天的情况下最为常见。为什么不直接让同一位工程师一开始就实现模型,而不是使用 AutoML 系统呢?
目前有人正在研究这个部署问题。例如,Modular 创建了一个统一的格式来支持所有模型,并将所有主要硬件供应商和深度学习框架集成到这种表示中。当模型使用深度学习框架实现时,可以将其导出到该格式,并在支持该格式的硬件上进行部署。
结论
针对我们讨论的所有问题,我对长期来看AutoML仍然充满信心。我相信这些问题最终会得到解决,因为自动化和效率是深度学习发展的未来。尽管AutoML今天尚未被广泛采用,但只要ML革命继续下去,它就会被广泛应用。
破坏分析的坏循环
意见
对技术工作的过度重视如何对分析造成灾难性影响
·
关注 发表在 数据科学进展 · 6 分钟阅读 · 2023 年 1 月 16 日
--
图片由作者提供。
反馈循环是强大的东西。正反馈可以产生 高度杠杆效应,而负反馈则可以 抑制变化。此外,这些模式对干扰可能 极其 抵抗。如果你不解决根本的驱动机制,反馈循环将继续存在。毕竟,你不能 向太阳泼水。
为什么总是谈论反馈循环?嗯,我怀疑有一个负反馈循环负责分析中的所有问题。就像其他反馈循环一样,这个循环很难打破。
我想你们都感受到了这种影响——我们的影响力从未像它应该的那样高。我们很少被视为思想伙伴。我们做了出色的技术工作,但几乎没有引起重要人士的关注。当然,我们在制定战略决策时有一席之地,但仅仅是作为 SQL 技术员。而且这不是因为没有尝试。我们构建了自助服务系统,我们举行了办公时间,我们创建了复杂的课程以提升我们的同行,但这些举措从未带来持久的变化。
那么这个循环是什么?其实很简单:我们所做的一切都过度强调技术工作,设定了我们只做技术工作的期望。我们对扩大影响力的最佳努力因此被削弱。接下来,我将讨论这个循环,证明它的普遍性,并最终分享我为什么认为工具在解决这个问题上至关重要。
像往常一样,如果你在寻找偏见,我是 Hyperquery的首席产品官,我们正在押注于更好的工具如何解决这种问题,但我会保持客观——我保证。
核心的坏反馈循环
初看起来,似乎有不少不同的反馈循环把我们困在这片无人区。我将介绍其中的一些,以便与你们产生一些共鸣,但我稍后会辩称它们都源于相同的根本反馈循环。但首先,让我们来看看这些常见的破碎模式:
-
“分析就是关于速度”。过去几年里,这一直是关于分析的一个常见的叙述。但是,如果你主要通过工作速度来衡量价值,你就得变得更快,这意味着减少解释时间,考虑业务影响。你的工作范围缩小,迫使你进入一个反应性支持角色。
-
仪表盘过度使用。每当我们制作新的仪表盘时,一方面,我们为业务其他部分提供了更大的自助服务能力。但如果我们过于依赖仪表盘作为主要的沟通媒介,我们就在延续一种数据显而易见的神话——即我们不需要解释。显然,我们需要制作仪表盘,但我们过于频繁地默认使用它们。我们本能地将利益相关者指向仪表盘,而不是理解他们的问题。他们在错误的方式上变得更有权力,他们对我们价值的逐渐狭隘理解将我们固定在数据策展的角色中。
-
我们跳跃到查询的速度太快。我们都做过这种事:你在与高管开会时,为了提供快速的价值,你开始进行少数派报告,进行现场编码。这种做法有其时机和场合,但我们过于急于采取这种方法,而应该思考——从我们已经进行的大量实验和深入研究中总结和提炼。我们未能提供杠杆价值,而是寻求对我们炫目的(确实无与伦比!)技术能力的认可。这静静地成为了别人对我们期望的价值。
-
我们的工具过于技术化。我们的工具是钉在棺材上的最后一根钉子。它们是为技术工作而设计的,以牺牲简单、清晰的传递为代价,提供这些功能。技术能力被置于所有事物之上。我们当然喜欢这样。这迎合了我们每个人心中渴望成为黑客的心理。但你的 SQL 查询和笔记本传达了你应该避免的那种简化表现——它们是以代码为先的,所以你被视作以代码为先。我们尝试通过在更易分享的工具中撰写工作来打破这种局面,但我们对自己规则的遵守往往较低。毕竟,为什么要使用大量工具,当少量工具就能搞定?
常见的线索开始浮现。行业叙事、数十年的商业智能文化、我们的工具,甚至是我们自身的傲慢,都将我们推向一个我们的价值被降级为拉取数据的世界。而谴责我们的基本反馈循环是:我们通过技术技能来展示和评价自己,因此他人也通过技术技能来评价我们。
图片来自作者。
就这样。这就是压迫我们的病理现象。其实很简单:我们渴望影响力,却在追求一种被视作交易技能的卓越表现——我们的 SQL 技能。我们渴望在数据之外有所影响,但我们的目光却只盯着数据。这可能是我们至今仍未证明分析主管价值的原因。或者为什么你还会看到分析总监为其他高管编写 SQL。或者为什么我们的工作是被动的,不管我们做什么似乎都无法摆脱这种循环。
技术工作当然是分析中的必要部分。你不能在没有提取和分析数据的情况下分享见解。我不是说我们应该停止——这显然是不可能的。但我们倾向于过度关注工作中的技术方面,以至于忘记了分析不仅仅是一个技术领域。分析是通过数据提供价值。“通过数据”部分确实是技术性的。然而,“提供价值”并不是。而后者才是真正产生杠杆效应的地方。
工具问题:你不能没有 YouTube 成为 YouTube 明星。
说到这些,我想引起大家对这一因果计算中一个被低估的杠杆的关注:我们的工具。我通常不认为工具是至关重要的。人员和过程是任何成功数据项目的命脉。打破我们技术困境的首要基础要求是思维方式的转变。
但有时工具确实至关重要。毕竟,你不能没有 YouTube 成为 YouTube 明星。而且我们无法忽视的是,我们的影响力受到可用工具的限制。我们的工具——IDE、Jupyter notebook、BI 工具——在很大程度上是技术性的(意料之中)。就像我们一样,我们的工具也专注于创造,而在这个过程中让我们陷入坏的循环。
我的主要论点是:正确的工具可以打破不良循环。例如,正确的工具可以打破我上面列出的循环:
-
打破“速度”循环: 我们的工具不应赋予我们更快地提取和分析数据的能力。它们应当赋予我们将上下文添加到工作中,并同样轻松快速地传递知识而非数据。
-
打破仪表板过度使用的循环: 我们的工具应该推动我们分享可以与上下文并置的可视化,而不是推动我们创建更多的仪表板。
-
推动对齐,而不是垃圾搜索: 促使我们朝着对齐和影响的方向前进,而不是首先进行查询。文字应该始终是首位,因为业务目标应该始终是首位。 SQL 或 Python 可以在之后跟进。
-
根本上不是以创造为先,而是以影响为先。 我们需要专注于核心:文字的传递和解释。因此,基本的原始要求应该是文字及其相关的生态系统。
我们需要一个能推动我们超越技术工作的工具——一个基于我们价值不仅来自于我们推送的代码,而是来自于我们创造的见解的信念的工具。我们正经历一个亨利·福特时刻,我们需要停止对更快马匹的渴望。最终,我们需要一个新颖的界面,能够让我们轻松地制作出提升我们的分析,而不是更多让我们更深入技术领域的工具。虽然我不能确定它会是什么样子,但我有我的怀疑,显然现在正是变革的时机。
最后的评论
反应性、帮助台分析、被忽视的建议——这些问题指向一个系统性问题:我们陷入了一个负反馈循环中。再多的水也无法扑灭这场火。是时候积极对抗我们被极客诱惑的倾向,并在此过程中认真重新考虑我们的工具。
多变量高斯分布下的异常检测基础
异常检测概述、多变量高斯分布的回顾以及在 Python 中实现基本异常检测算法的两个示例
·
关注 发表在 Towards Data Science · 11 min read · 2023 年 7 月 5 日
--
我们与生俱来的模式识别能力使我们能够在填补空白或预测未来事件时运用这一技能。然而,有时会发生不符合我们期望的情况,超出了我们对模式的认知。这些情况被称为异常。如果我们尝试预测某些事物,我们可能希望将异常数据排除在训练数据之外。或者我们可能希望识别异常以改善我们的生活。在这两种情况下,异常检测技术都能在大多数行业和学科领域中发挥作用。
本文将引导你了解异常检测的基础知识以及统计异常检测模型的实现。
什么是异常检测?
一般而言,异常检测是指识别异常现象的过程。异常检测的目标是识别那些与我们的期望不符的事件、发生情况、数据点或结果。因此,实施异常检测的关键是理解预期事件的基本模式。如果我们知道预期的模式,我们可以用它来映射前所未见的数据点;如果我们的映射不成功,而我们的新数据点落在预期模式之外,那么很可能我们发现了异常。
通常有三种类型的异常情况。第一种类型包括相对于整个数据集被认为异常的单独实例(例如,一辆汽车在高速公路上以非常低的速度行驶,相比于所有高速公路交通,这种情况被认为是异常的)。第二种类型包括在特定背景下的异常实例(例如,相比于所有信用卡交易,看似正常的信用卡交易在特定个人的消费模式中却是异常的)。第三种异常类型是集体性的——即使每个实例本身都符合一定的期望,一组实例也可能被认为是异常的(例如,一次单独的欺诈性信用卡交易在亚马逊上可能不显得特别,但在短时间内连续发生的一组交易则很可疑)[1]。
异常检测技术分为三类:
-
监督检测需要数据集中有正标签和异常标签。可以应用诸如神经网络或提升森林等监督学习算法,将数据点分类为预期类别/异常类别。不幸的是,异常数据集往往非常不平衡,通常没有足够的训练样本来使上采样或下采样技术帮助监督学习。
-
半监督检测处理部分标记的数据。半监督技术假设输入数据仅包含正实例,并且输入数据遵循预期模式。这些技术试图学习正案例的分布,以便能够生成正实例。在测试过程中,算法将评估异常实例可能是由模型生成的概率,并使用这一概率来预测异常情况。[2]
-
无监督检测使用完全未标记的数据来创建期望的边界,任何落在此边界之外的都被认为是异常的。
异常检测技术可以应用于任何数据,数据格式会影响哪个算法最为有效。数据类型包括 序列(时间序列、链表、语言、声音)、表格(例如,发动机传感器数据)、图像(例如,X 光图像)和 图(例如,工作流或过程)。
胸部 X 光图像中的异常检测示例 [4]
由于问题和技术的多样性,异常检测实际上是数据科学的一个广泛领域,应用范围广泛。其应用包括:欺诈检测、网络安全应用、销售或交易数据分析、稀有疾病识别、制造过程监控、外星行星搜索、机器学习预处理等。因此,获得强大且高效的算法有可能在许多领域产生重大影响。
让我们看看可以用来检测异常的最基本算法。
用于异常检测的高斯分布
基本的异常检测技术之一利用高斯(即正态)分布的力量来识别异常值。
高斯分布由卡尔·弗里德里希·高斯发现,它建模了许多自然现象,因此是建模数据集特征的热门选择。该分布的概率密度函数是一个以算术平均数为中心的钟形曲线,曲线的宽度由数据集的方差定义。大多数情况都在中心附近,概率密度函数的两端有两个拉长的尾部。实例越稀有——距离中心越远——它成为异常值的可能性就越大。 Eureka!——我们可以利用这个概念来建模数据集中的异常。
概率密度函数,定义为 f(x),测量数据集中某个结果 x 的概率。正式地,
假设我们的数据集只有一个特征,并且该特征遵循正态分布,那么我们可以使用上述的 f(x) 来建模我们的异常检测算法。然后我们可以设置一个阈值 epsilon 来决定一个情况是否异常。Epsilon 应根据启发式方法设置,其值将依赖于使用场景和对异常的敏感度。
描述正态分布的图 [5]
在正态分布中,2.5% 的实例发生在均值下方两个标准差处。因此,如果我们将阈值设置为 0.054,那么在我们的数据集中,大约 2.5% 的事件将被分类为异常(均值下方两个标准差的 CDF 为 2.5,而 -2 的 PDF 为 0.054)。较低的阈值将导致分类的异常更少,而较高的阈值则敏感度较低。
在实际应用中,可能会存在一个权衡,因为一些正例可能会低于阈值,而一些异常值可能会隐藏在阈值之上。理解使用案例并测试不同的 epsilon 值是必要的,然后确定最适合的值。
单特征的例子很简单——如果我们有多个特征怎么办?如果我们的特征完全独立,我们实际上可以取特征概率密度函数的乘积来分类异常。
对于两个不相关特征的情况,这变成
本质上,特征概率的乘积可以确保如果至少有一个特征存在异常值,我们可以检测到异常(假设我们的 epsilon 足够高);如果我们的实例在多个特征中表现出异常值,我们的概率将更小(因为我们的总概率值是各个分数的乘积),从而更有可能将一个值识别为异常。
然而,我们不能假设我们的特征是独立的。这就是多变量概率密度函数发挥作用的地方。在多变量情况下,我们构建一个协方差矩阵(用 Σ 表示)以捕捉特征之间的关系。然后,我们可以利用协方差矩阵来避免“重复计算”特征关系(这是一种非常初步的表述实际发生的情况)。多变量分布概率密度函数的公式如下,这些来自杜克大学的幻灯片很好地推导了这个公式。
在这里,x 是一个输入向量,μ 是特征均值的向量,Σ 是特征之间的协方差矩阵。
为了简化我们的工作,我们可以使用 scipy 库来实现这个函数:scipy.stats.multivariate_normal 以特征均值和标准差向量作为输入,并且具有一个 .pdf 方法用于根据一组点返回概率密度。
让我们在实际示例中尝试这个实现。
两特征模型在 Python 中的实现
首先,让我们观察一个具有两个特征的示例,这将使我们能够在欧几里得空间中可视化异常点。对于这个示例,我生成了两个特征,分别从正态分布中抽取了 100 个样本(这些是正样本)。我计算了特征均值和标准差,并利用 scipy.stats 库中的分布信息拟合了一个多变量正态模型。值得注意的是:我仅用正样本拟合了我的模型。在实际数据中,我们需要清理数据集,以确保特征符合正态分布,并且没有异常值或奇异值——这将提高模型定位异常的能力(尤其是有助于确保特征的正态分布要求)。最后,我向数据集中添加了 5 个异常样本,并使用 .pdf 方法报告概率。
下图的散点图展示了结果:x1 特征绘制在 x 轴上,x2 特征绘制在 y 轴上,异常点已标注,颜色表示来自多变量概率密度函数的概率。
展示正样本和异常点的散点图 [5]
一旦我们将阈值设置得足够低,我们将能够区分异常值和期望值。下面的两个图表比较了 1x10^-7 和 1x10^-9 的 epsilon 值。1x10^-9 的 epsilon 值往往能更好地捕捉我们预期的异常值,而 1x10^-7 则将一些正样本识别为异常值。
在较高和较低 epsilon 值下识别的异常点的散点图比较 [5]
在这个示例中,很容易识别出 epsilon,因为我们可以直观地描绘和识别异常点并分析结果。让我们看看在具有更多特征的示例中情况如何变化。
使用 Python 实现多变量模型
在这个示例中,我将使用 葡萄酒数据集 来自 ODDS 库 [3]。该数据集包含 13 个数值特征和 129 个实例。特征捕捉了有关葡萄酒的信息,原始数据集用于基于葡萄酒分析的分类任务。为了进行异常检测,将目标类别中的一个类进行了下采样,并作为异常点呈现。总共有 10 个异常点,129 个实例中约有 8%。我们处理的是一个相当干净的数据集,没有缺失值。
我们首先必须确保我们的特征符合高斯分布。尽可能的话,我们应当去除异常值,并使用归一化策略对分布进行归一化。在这个数据集中,4 个特征已经符合正态分布(酒精、灰分、灰分的碱度和非黄酮酚)而 4 个特征可以通过取对数来进行归一化(总酚、前花青素、颜色强度和色调)。虽然对剩余特征存在更好的策略,但为了这个练习的目的,我仅仅从我们的训练数据集中删除了它们。最后,我通过排除所有包含至少一个特征值超出或低于均值 2 个标准差的行来去除异常值。其余代码与上述示例相同。
与上节中的两个特征示例不同,现在不再可行在二维平面上可视化结果,但我们可以使用混淆矩阵指标(包括召回率和精确度)以及 ROC 曲线下面积来帮助我们找到适用于用例的正确 epsilon。
由于通常存在精确度与召回率之间的权衡,epsilon 的设置取决于我们用例的敏感度要求。在这个例子中,我寻找一个可以最大化曲线下面积的 epsilon 值。一些用例可能要求尽可能多地找到异常(以包括正值为代价),而其他用例可能只在绝对确定的情况下检测异常(以遗漏一些异常为代价)。我为几个不同的 epsilon 值计算了评估指标。
按 epsilon 值的评估指标线图比较 [5]
随着 epsilon 的增加,召回率提高。尽管在提出的 epsilon 值范围内精确度相对较低,但通常在 0.0035 和 0.0065 左右达到峰值。AUC 尝试在精确度和召回率之间取得平衡,并在 0.0065 附近达到峰值。让我们来看看混淆矩阵。
描述混淆矩阵的表格 [5]
我们的模型在发现所有异常点方面表现相当出色,只漏掉了一个。考虑到我排除了三分之一的特征,这个结果非常棒。不幸的是,我们的模型还将 40 个正实例标记为异常,这意味着如果我们使用这个模型进行异常检测,我们必须手动检查一半的正实例,以确认它们是否确实异常。
为了改进该模型,我们可以进一步工程化剩余特征,并找到可能对异常值稍微不那么敏感的 epsilon 值。这个问题的其余部分是简单的,留给读者作为练习(iykyk)。你可以在这里找到源代码。
高斯异常检测的潜在缺陷
多变量高斯分布是异常检测的一个很好的模型——它简单、快速且易于执行。然而,它的缺点可能会阻碍它在众多使用案例中的应用。
第一,多变量分布可能产生相当低的概率密度值。通常,这对现代计算机来说不是问题。但有时这些值可能会低到计算机无法有效处理的程度。
第二,我们必须确保我们的特征遵循正态分布。如果投入时间和精力进行适当的特征工程和数据处理,这可能不会是一个大问题,但投入的努力是有风险的,因为我们无法在工作完成之前知道回报。
第三,该模型无法处理分类特征。如果我们的数据集中包含分类特征,我们必须为每种分类特征的组合创建一个单独的模型(这可能会成为一项繁重的工作)。
最后,该模型假设所有特征具有同等重要性,并且特征之间没有复杂的关系。处理此问题的一种选择是从头开始实现多变量分布概率密度函数,并包括一些参数来帮助处理特征重要性。为了处理特征关系问题,我们可以进行进一步的特征工程,创建新特征,但这个过程可能会很困难、耗时且有风险(就回报而言)。
尽管如此,使用多变量高斯分布进行异常检测是解决表格数据异常检测问题的一个很好的第一步。它可以用于设定基准,或作为捕捉数据集中异常的完美工具,并为我们提供了一种直观理解异常检测的方法。
感谢阅读!在不久的将来,我希望能做一个关于异常检测的完整系列,如果你对这个话题感兴趣,请继续关注。
来源:
-
www.kaggle.com/code/matheusfacure/semi-supervised-anomaly-detection-survey
-
ai.googleblog.com/2023/02/unsupervised-and-semi-supervised.html
-
Saket Sathe 和 Charu C. Aggarwal。 LODES: Local Density meets Spectral Outlier Detection. SIAM 数据挖掘会议,2016 年。
-
Nakao, T., Hanaoka, S., Nomura, Y. 等。《胸部放射图中的无监督深度异常检测》。J Digit Imaging 34,418–427(2021)。
doi.org/10.1007/s10278-020-00413-2
数学排版由 Codecogs 在线 LaTeX 编辑器 提供。
带有示例的 Jupyter Notebook 可以在这里找到。
对比增强 CT 基础知识
静脉对比增强对于 CT 成像至关重要。它可能如何影响你的 CT 图像数据集
·
关注 发表在 Towards Data Science ·7 分钟阅读·2023 年 3 月 8 日
--
“风格如文森特·梵高的脑部 CT”。 DALL-E 2.
对比剂的使用在放射成像中无处不在,但网上关于这个主题的信息却出奇地少。在这里,我将描述 IV 对比剂的基础知识,它在 CT 图像中的表现,以及它可能如何影响你的成像数据集。
作为从事医学影像研究的数据科学家,了解对比剂在数据集中的使用情况非常重要,因为对比剂会强烈影响 CT 检查的外观,包括强度和纹理。根据应用场景,对比剂可能是成像特征的主要来源,或者是一个不希望出现的变异来源。你还会发现对比剂的使用可能很微妙,有时标记也不准确,因此对这一主题有良好的工作知识将帮助你更有效地组织数据集和规划研究。
CT 衰减基础
要理解为什么在 CT 检查中使用对比剂,首先必须了解一点 X 射线物理学。CT 扫描仪是一个 3D 成像系统,它测量被体内器官和组织吸收的 X 射线能量。在扫描仪内部,旋转的 X 射线管将高能光子射穿身体并进入探测器阵列,探测器测量光子能量的衰减率或衰减。
在计算机断层扫描中,Radon 变换用于将旋转的 1D X 射线投影重建为 2D 轴向图像。GIF 来自WikiMedia。
在体内,密度较高的结构如皮质骨会阻挡更多的光子,增加衰减率。这些高衰减区域在最终扫描中会显得非常明亮。
密度较低的结构,如充满空气的肺部,会通过光子并显得较暗。衰减以Hounsfield 单位(HU)(以 CT 发明者戈弗雷·侯恩斯菲尔德命名)报告,并进行标准化,使空气的衰减值为-1000HU,水的衰减值为 0HU。脂肪组织的典型衰减值为-100 到-50HU,血液的衰减值为+30HU,而软组织的衰减值为+10 到+50HU。
对比材料
对比剂的目的是突出那些否则很难与周围环境区分开的组织。如果没有对比剂,体内大部分软组织的 X 射线吸收非常相似,会显得均匀的灰色。
对比剂通常是静脉注射(注射到静脉中)或口服(吞咽),尽管对比剂也已被改编为所有可能的给药方法。口服对比剂用于突出胃、小肠和大肠的胃肠道成像。静脉(IV)对比剂更为常见,应用于多种指示(也可以与口服对比剂结合使用)。
在这两种情况下,对比剂由惰性重金属如碘或钡组成,这些重金属悬浮在液体溶液中。重金属具有非常高的衰减率,优先阻挡 X 射线能量,在最终 CT 图像中显得明亮。
静脉对比在 CT 中的表现
静脉对比度捕捉了血液在体内的流动。最简单的层面上,血液更多的组织会显得更亮,或增强。在下面的腹部 CT 检查中,左侧 CT 是在注射对比剂前获得的,右侧 CT 是在注射后获得的。脂肪组织(脂肪)血流少,在两幅图像中表现相似,而肝脏和脾脏则由于接受高血流,在最右侧的图像中显得更亮(更增强)。注意最大区别在于主动脉以及肝脏和脾脏的血管。
无对比(左)和后对比(右)腹部 CT。图片来自 Radiopaedia,注释由作者添加。
下面展示了另一个例子,一个来自经历出血性中风的个体的后对比头部 CT 扫描。扫描中明亮的对比剂区域指示了出血的位置和严重程度。
CT 血管造影点征标示了中风患者出血的位置和严重程度。图片来自 Radiopaedia。
对比阶段捕捉了随时间变化的灌注变化
循环是一个动态过程。考虑将少量对比剂注入手臂的静脉(为了确保注射的时间保持一致,对比剂通过泵注入,有时压力高达 300psi)。这种注射量的对比剂需要几秒钟才能到达心脏和肺部。从心脏,它将经过主动脉,分支到主要动脉,再通过毛细血管系统进入体内组织,最后通过静脉循环返回心脏。在多个心动周期中,肾脏会开始从血液中滤除对比剂,并将其排泄到膀胱中。
一名女性接受对比 CT 检查。照片来自 Wikimedia Commons。
CT 对比成像利用循环的这一动态特性,通过在注射后一个或多个特定时间点,或阶段捕捉图像。
数百种 CT 成像协议捕捉了这些对比阶段的各种组合。这些阶段对比 CT 将精确地定时到对比剂注射的时刻,定时会根据扫描目的和要求测试的放射科医师的偏好有所不同。
例如,对比增强特别有用来成像肾脏,肾脏有一个特征性的灌注模式。为了捕捉这一点,影像学协议被定时以捕捉对比剂通过肾脏的精确时刻。我在研究生阶段参与的一个出版物是对使用肾脏对比 CT 训练的分类器的可解释 AI 技术的验证。
肾脏影像学的常见阶段。来自一份在线发布的案例报告。
一般而言,在所有指示下,CT 对比阶段被简化为 4 个类别:
-
前对比或非对比,体内没有静脉对比剂(检查可能在注射对比剂之前进行,或可能根本没有使用对比剂)。
-
动脉期,通常在注射后 15–30 秒,对比剂在心脏、主动脉和动脉中进行灌注。
-
静脉期,通常在注射后约 60 秒,对比剂在组织中进行灌注并返回循环。
-
延迟期或肾图像期,通常在注射后 120–180 秒,对比剂正在被肾脏过滤并从循环中去除。
一项 CT 研究可能只有一个对比阶段,可能有所有四个阶段,或者任何组合。
你需要知道的关于数据科学项目的事项
首先,要意识到对比剂的使用会影响体内几乎每种组织的外观,除了脂肪组织和游离液体;即使是你可能不预期的组织,如骨头,基于对比剂的使用,其强度可以变化 20–30HU。
对比剂系列的组合将根据指示和下单检查的放射科医生的偏好而有所不同。协议也会定期更新。这意味着如果你将来自不同患者群体、不同医院或不同扫描仪的检查组合在一起,你的数据集中可能会出现不一致的对比检查组合。
处理这个问题有两种方法。如果你使用 CT 进行其预期的应用(例如,你在对头部 CT 的胶质母细胞瘤病例进行建模),请准确确定要包括哪些 CT 对比阶段,并将其作为选择标准。例如,你可能要求所有胶质母细胞瘤病例必须有前对比和动脉期对比扫描;如果检查没有这种确切的组合,则将其排除。额外的系列也会被排除。这通常是在讨论研究设计时与放射科医生进行的对话。
如果你使用 CT 进行一种偶发应用,你通常可以更加宽容。例如,我们的骨密度模型可以将脊柱分割出来,是在所有四个对比相位图像上进行训练的。如果病人有多个对比相位序列,我们只需将所有相位上的骨测量值平均即可。我们进行了全面的验证,证明静脉注射对模型准确性没有影响,只对后续密度测量产生轻微影响(误差不超过 10%),我们承认这是我们目前方法的局限性。
最后,这可能是最不幸的事实,对比相位通常没有很好的标注。通常在系列描述中用缩写形式表示,但这些可能很难解释。DICOM 头部有一组标签,从(0018, 0010) 对比剂注射剂开始,但我发现这些报告不一致。我发现的唯一可靠的方法是通过阅读图像本身来准确标记对比相位,并且有 100% 的信心。
CT 对比相位是一个在深度学习的最新突破中有望受益很多的领域。我的同事兼好朋友 Gian Marco Conte 在《放射学杂志》发表了一篇文章,他训练了一个 GAN 来合成脑胶质母细胞瘤缺失的 MRI 序列,类似的方法也可以用于合成 CT 对比相位。
作为一个病人...
虽然我从未进行过 CT 检查,但我个人做过几次静脉注射 MRI,我可以向你保证,使用对比剂是无痛的。在程序开始之前,在你的手臂上放置了一根静脉注射管,在检查过程中,当注射对比剂时,你会感到一阵冷风吹到你的手臂上。有些人声称他们口中有一种金属味,但我个人没有经历过这种感觉。
信息提取的起始:突出关键字并获取频率
一种快速的方法,用于在 PDF 文档中突出显示感兴趣的关键字并计算其频率。
·发表于 数据科学 ·10 分钟阅读·2023 年 8 月 28 日
--
介绍
随着每天可用信息量的增加,能够快速收集相关统计数据对于关系映射和获取对重复数据的新视角变得至关重要。今天我们将深入探讨 PDF 文本提取,也称为信息提取,以及对不同语料库进行快速事实和想法的形成。今天的文章深入探讨了自然语言处理(NLP)的领域,这是计算机理解人类语言的能力。
信息提取
信息提取(IE),正如 Jurafsky 等人所定义的,是“将嵌入文本中的非结构化信息转化为结构化数据的过程。”[1]。一种非常快速的信息提取方法不仅是搜索以查找某个词是否出现在文本中,还包括计算该词提到的频率。这一点的支持是基于一个词在文本中出现得越多,它就越重要,并且与语料库的主题关系越大。 重要的是要注意,停用词的去除对于这个过程非常重要。为什么?因为如果你简单地计算语料库中所有单词的频率,像the这样的词会被提到很多次。这是否意味着这个词在传达文本信息方面很重要?不,因此你需要确保你关注的是那些有助于语料库语义意义的词汇频率。
信息提取(IE)可以导致在文档中使用其他自然语言处理技术。这些技术超出了本文的代码范围,但我认为它们既有趣又重要,因此值得分享。
第一种技术是命名实体识别(NER)。正如 Jurafsky 等人详细说明的那样,“命名实体识别(NER)的任务是找到文本中每个命名实体的提及并标注其类型。”[1] 这类似于在文本中搜索单词频率的想法,但 NER 更进一步,通过使用单词的位置以及其他文献规则来寻找文本中的不同实体。
信息提取(IE)还可以支持另一种技术,即关系抽取,这就是“在文本实体之间找到并分类语义关系抽取。”[1]。目标不仅是提取文本中不同词汇出现的频率以及它们所属的实体标签,还要如何将这些不同的实体联系在一起,以形成语料库的潜在语义意义、模式或摘要。
之前的三个任务都与事件抽取的目标相关。Jurafsky 等人进一步指出,“事件抽取是寻找这些实体参与的事件。”[1]。这意味着,通过从文本中寻找和提取不同的统计数据和标签,我们可以开始阐述关于这些事件发生的假设,并且通过这些实体了解不同事件之间可能存在的关系。
过程
过程(图像来源于作者)
该过程的目标是快速了解一个文本语料库是否不仅包含与您所寻找的信息相关的内容,而且还提供频率数据,以便判断是否有足够的信息来进行不同文档之间的比较。最后,它提供了查询词在文本中高亮显示的可视化,这有助于根据周围的词语得出关于关系和文本内容的结论。整个过程是用 Python 在 Google Colab 中实现的,可以轻松地转移到您选择的任何 IDE 中。让我们看看代码吧!
代码
这个代码所需的 Python 库是PyMuPDF和Counter。
import fitz # PyMuPDF
from collections import Counter
接下来,我们将创建一个名为“highlight_terms”的函数,该函数接受一个输入 PDF,一个输出 PDF 的路径,一个输出文本文件的路径,以及我们希望高亮显示的术语。
def highlight_terms_and_count(input_pdf_path, output_pdf_path, terms_to_highlight, output_text_file):
"""
A function which accepts a PDF file and a sting of words as input
and outputs a highlighted PDF file of the queried words and a text file
with the query word frequences.
Arguments:
input_pdf_path (str): Path to a PDF file
output_pdf_file (str): Path to the output pdf file
terms_to_highlight (list): List of terms (str) to highlight
output_text_file (str): Path to output text file.
Returns
output_pd_file : A PDF highlighted with the queried words.
output_text_file: A text file containing the frequency of each queried word.
"""
# Open the PDF file
pdf_document = fitz.open(input_pdf_path)
term_counter = Counter()
for page_number in range(len(pdf_document)):
page = pdf_document[page_number]
# Get the text on the page
text = page.get_text()
for term in terms_to_highlight:
term_instances = page.search_for(term)
term_counter[term] += len(term_instances) # Count term instances on this page
for term_rect in term_instances:
# Create a highlight annotation
highlight = page.add_highlight_annot(term_rect)
# Set the color of the highlight (e.g., yellow)
highlight.set_colors(stroke=(1, 1, 0))
# Set the opacity of the highlight (0 to 1)
highlight.set_opacity(0.5)
# Save the modified PDF
pdf_document.save(output_pdf_path)
pdf_document.close()
# Save term frequencies to a text file
with open(output_text_file, 'w') as text_file:
for term, frequency in term_counter.items():
text_file.write(f"{term}: {frequency}\n")
一旦我们创建了函数,就可以设置文件路径到我们的不同目录。
if __name__ == "__main__":
input_pdf_path = "/content/AlexNet Paper.pdf" # Replace with your input PDF file
output_pdf_path = "/content/output.pdf" # Replace with your output PDF file
terms_to_highlight = ["neural", "networks"] # Add the terms you want to highlight
output_text_file = "/content/term_frequencies.txt" # Text file to store term frequencies
highlight_terms_and_count(input_pdf_path, output_pdf_path, terms_to_highlight, output_text_file)
就这样,简单如斯!这对于分析时快速了解某个词(或词组)在文档中的相关性非常有帮助。此外,当查看高亮显示的输出 PDF 时,标出词语的位置可以帮助引起您的注意,并与查询词周围的其他词语建立关系。
示例
在今天的示例中,让我们看看来自 Google Scholar 的一篇学术文章。如果我想知道一篇文章是否有足够的信息关于Neural Networks,该怎么办呢?我们可以查询文档,然后使用查询到的频率来评估文档是否集中于神经网络。我们将查看的第一篇文档标题为ImageNet Classification with Deep Convolutional Neural Networks[2]。
比如,我们对研究神经网络感兴趣,并想查看这些词在文本中出现的次数和位置。通过查询Neural和Networks,我们将得到一个高亮显示的 PDF,如下所示。
此外,还将创建一个文本文件,记录Neural和Network这两个词的出现频率,分别为 21 和 23。词频的多少确实取决于用户是否认为某个词在文档中提及的次数足够,以至于被认为是相关的。由于这两个词在文档中出现超过 20 次,我认为它们与文档的上下文相关。
这个过程可以轻松扩展到多个文档中,然后可以比较不同文档之间某些词的频率。通过这样做,我们可以比较文档,并得出哪些文档与我们寻求的信息更相关的结论。
限制
虽然这种方法简单且能够完成所需的任务,但还是有一些值得讨论的限制。
-
词汇在语料库中出现的频率越高就越能支持其潜在含义的假设并不总是成立。
-
查询错误的词汇可能导致你无法揭示文本的正确含义或找到你所寻找的信息。
-
该方法论更多地是对信息提取的总结,并为更深入的分析提供了一个起点。
关于假设的第一个局限性认为,词汇使用得越频繁,就越能支持文本的潜在含义,这取决于作者的发表方式和他们如何表达自己的观点,并不总是成立。此外,不同的词汇可以用来描述相同的含义,而仅关注几个高频词汇,你可能会发现自己对文本语料库附加了错误的含义。
第二个局限性来自于第一个关于词汇查询的限制。如果你没有查询正确的词汇,可能会错过文本语料库的重要潜在含义。克服这一点的一种方法是使用 PyMuPDF 或 PyPDF2 将 PDF 转换为文本文档,然后计算出现频率最高的 N 个词。从这些频率中,你可以找出这些词在文本中的位置,这有助于你找到最重要的信息所在。
今日方法论的第三个局限性是它没有深入到信息提取的层面。不过,这段代码确实很好地为你开始文本提取项目奠定了基础,并可以衍生出几个不同的方向。以下是一些你可以从这个过程继续发展的可能方式:
-
开始比较文档,选择某个词及其频率较高的文档。
-
评估文档中突出显示的区域,并创建一个脚本来提取这些区域。然后,对每个句子进行情感分析。
-
与刚才提到的方法相同,只不过现在要执行主题建模分析。任何被突出显示的句子都将用于支持文本中的不同主题。
优势
尽管我提到这个过程存在一些局限性,但它确实有一些好处,我相信你会发现它的使用有些实用之处。首先,它可以展示一个词在不同文本中的相关性。这在学术研究中尤其有帮助,特别是当你在研究某个主题时。如果我在研究机器学习,而一篇学术文章提到它的频率比另一篇高,我可能更倾向于先查看那篇文章。一旦我选择了一篇文章,这个过程将突出显示词汇在该文章中的位置。与其盲目地筛选文献,这个过程将我们的注意力集中在文本中的信息所在之处。说到节省时间!
这个过程的另一个好处是你可以开始标记某些句子的位置,这可能对你引用不同文档中的信息时非常有用。能够追溯到你找到这些信息的地方不仅对提供证据给你的观众很重要,也是一种确保你不会抄袭的好方法。我辅导了许多学生,并且使用这个过程为他们标记不同的文章,以便他们看到我从哪里获得我的工作(根据我们研究的主题),并将标记的文档作为学习的参考。
最后,这个过程是你文本提取项目的绝佳起点。它收集初步统计数据,以开始塑造项目方向,并提供一个可视化的文本理解工具,并为客户提供内在含义的证据。一个真实的例子是,我为一位主要在食品行业工作的客户开发了一个项目。他们想知道他们收到的这份巨大文档(数百页)是否提到他们寻求销售的商品,并且还想知道这些商品与谁相关。我能够查询他们销售的不同商品,并将文档中的产品高亮显示,以便快速参考。他们非常喜欢!我的唯一建议是开始注释页码,这将进一步缩短分析时间!
结论
今天我们讨论了如何分析 PDF 文档,找到并定位文档中的感兴趣词汇。此外,我们还可以收集查询词汇的频率,以更好地理解文本语料的可能含义,以及是否与我们想要深入了解的内容相关。信息提取至关重要,因为它不仅帮助我们发现不同实体之间的关系,还能提供关于这些相关实体和不同文档中可能出现的行为模式和事件的洞察。试试这个代码,希望它对你下一个自然语言处理项目有所帮助!
如果你喜欢今天的阅读,请关注我,并告诉我是否有其他话题你希望我深入探讨!如果你没有 Medium 账户,可以通过我的链接 这里 注册(这样我会获得小额佣金)!此外,可以在 LinkedIn 上添加我,或者随时联系我!感谢阅读!
来源
1 最佳替代 Seaborn Distplot 的 Python 库
原文:
towardsdatascience.com/the-best-alternative-to-seaborn-distplot-in-python-ea95391e6d29
数据科学
Seaborn Distplot 已被弃用——让我们探索它的替代品
·发表于 Towards Data Science ·8 分钟阅读·2023 年 6 月 14 日
--
图片来源:Bon Vivant 于 Unsplash
Seaborn 是 Python 中一个著名的数据可视化库。
由于它建立在 matplotlib 之上,并且与 pandas 数据结构完美兼容,因此在 Python 中处理数据时非常方便,因为它将数据转化为有洞察力的可视化图表。它有助于专注于所需的信息并更快地掌握结果。
然而,每个库都会随着时间的推移而发展,Seaborn 也是如此。
当我在项目中使用 Seaborn 创建分布图时,遇到了函数弃用的警告,如下所示。
图片来源:作者
所以,我开始寻找替代品,并今天分享我的发现。
在这篇简短的文章中,你将了解为什么 Seaborn 弃用了出色的函数distplot()
,目前的最佳替代方案是什么,以及如何使用它创建与distplot()
相同的图表。
这里是内容的预览——
· Seaborn 中的 Distplot · 为什么 Seaborn Distplot 被弃用? · Seaborn Distplot()的替代方案有哪些? ∘ Seaborn 中的 displot() · displot()在 seaborn 中的使用场景 ∘ 双变量分布 ∘ 数据子集的图表
我选择了令人惊叹的例子来使这篇文章有趣,并使用了来自 UCI 机器学习库的 Dry Beans 数据集,该数据集在 CC BY 4.0 许可下提供。
Dry Beans 数据集 | 图片来源:作者
让我们深入了解吧!
在查看替代方案之前,首先了解一下 distplot()
函数及其用途。
Seaborn 中的 Distplot
Distplot()
是 Seaborn 库中的一个多功能函数,广泛用于单变量数据分析。它帮助你在同一可视化中创建直方图和核密度估计(KDE)图。
什么是单变量数据分析?
它用于一次探索单个变量的特征和分布,而不考虑其与数据集中其他变量的关系。
回到 distplot()
,它由直方图和 KDE 图组成。
Distplot()
中的直方图显示了落入不同桶(即箱)的数据点的频率或计数。
整个数据点序列或列表被分箱成不同大小的桶。可视化图形只是一个条形图,其中 X 轴通常是桶或箱,Y 轴表示桶中的数据点数量。
这样的图有助于你了解数据如何在数值范围内分布。
而 KDE 图则通过分析基础概率分布函数来帮助你可视化变量的分布。简单来说,它帮助你理解在不同的桶或箱中观察数据点的可能性。
使用 KDE 图,你可以了解数据分布的形状、峰值及其扩展范围。
例如,我们可以对“Perimeter”这一列使用 distplot()
函数。
import pandas as pd
import seaborn as sns
df = pd.read_excel("Dry_Bean_Dataset.xlsx")
sns.distplot(df["Perimeter"])
Seaborn distplot() | 作者提供的图片
如上图所示,条形图表示直方图,而平滑的线条表示 KDE 图。
正如我提到的,distplot()
在已经创建的直方图上创建了一个 KDE 图,这就是为什么在 Y 轴上你可以看到概率密度值的原因。
不要混淆概率与概率密度!
你需要将概率密度乘以曲线下的面积,以从每个概率密度值中获得概率。
这样的 KDE 值只能用于不同桶之间的相对比较。
现在,让我们理解一下为什么将来不应该使用它。
为什么 Seaborn 的 Distplot 被废弃了?
Distplot()
是 Seaborn 库中最早添加的几个函数之一,因此其函数定义与后续添加的其他函数有显著不同。
这是根据 Seaborn 官方文档 的 distplot()
函数定义。
seaborn.distplot(a=None, bins=None, hist=True, kde=True, rug=False,
fit=None, hist_kws=None, kde_kws=None, rug_kws=None, fit_kws=None,
color=None, vertical=False, norm_hist=False, axlabel=None, label=None,
ax=None, x=None)
迈克尔·瓦斯科姆 精确地解释了——distplot()
API 既没有用于选择 DataFrame 列的 x、y 参数,也没有条件 hue
映射。
所以,当 Seaborn 开发者在更新 Seaborn v0.11.0 中的分布模块时,他们发现没有比废弃 distplot()
更好的方法来使其与其他分布图函数保持一致。
结果是 Seaborn 的distplot()
在 Seaborn v0.11.0 中已被弃用。
调用这个函数并不会真正阻止你创建图,但它会发出一个弃用警告,如我之前提到的。
这就是为什么我开始探索其他替代方案。
Seaborn Distplot()的替代方案是什么?
Seaborn 文档建议了两个替代方案——displot()
和histplot()
。但我个人发现displot()
是一个多功能的解决方案。
让我展示一下与已弃用的distplot()
相比,它有多相似或不同。
Seaborn 中的 displot()
这是一个适用于所有类型(单变量和双变量)分布图的一站式解决方案。你需要做的就是传递一个 DataFrame 和你想查看分布的列名。
所以,要获得类似上面的分布图,你可以使用以下代码来处理‘Perimeter’列。
sns.displot(df, x="Perimeter")
使用 seaborn displot()的直方图 | 图片作者
它简单地创建一个与已弃用的distplot()
函数创建的直方图相同的直方图。你可以使用函数histplot()
获取这种类型的图,它是已弃用函数的另一个替代方案。
那么 KDE 图呢?
你也可以使用displot()
函数获取 KDE 图。这时,**kind**
参数就会发挥作用。你可以将**kde**
赋值给kind
参数,以获取如下所示的 Kernel Density Estimate 图。
sns.displot(df, x="Perimeter", kind='kde')
使用 Seaborn displot()的 KDE 图 | 图片作者
到目前为止,一切都很好!
但你可能会有一个问题——displot()
函数如何创建一个类似于distplot
的图,其中 KDE 绘制在直方图的顶部?
答案是——**kde**
参数。
正如你所见,默认情况下displot()
创建一个直方图。因此,要在直方图上方创建 KDE 图,你可以将**kde**
参数设置为**True**
,如下所示。
sns.displot(df, x="Perimeter", kde=True)
Seaborn 使用 displot()的直方图和 kde 图 | 图片作者
使displot
函数与distplot
不同的真正原因是上面图表的 Y 轴。
在已弃用的函数distplot()
中,Y 轴表示概率密度,而在函数displot()
中,Y 轴表示计数,即每个箱子的数据显示点数量。
Y 轴上的计数可以直接帮助你理解哪个箱子或值的范围包含最多/最少的数据点,而这在概率密度情况下并非如此。
好吧,displot()
函数的灵活性不仅仅止于此。让我展示一下你可以用这个函数做什么,这在distplot()
中是比较困难的。
seaborn 中 displot()的使用案例
函数displot()
具有大量的参数,你可以调整这些参数来创建各种图形。
seaborn.displot(data=None, *, x=None, y=None, hue=None, row=None, col=None,
weights=None, kind='hist', rug=False, rug_kws=None,
log_scale=None, legend=True, palette=None, hue_order=None,
hue_norm=None, color=None, col_wrap=None, row_order=None,
col_order=None, height=5, aspect=1, facet_kws=None, **kwargs)
你可以在上述定义中看到,kind
参数默认设置为 ‘hist’
,这解释了为什么 displot()
在未指定 kind
参数时创建直方图。
我不会详细讲解这些参数,但我必须提到一些有趣的参数。
双变量分布
displot()
函数能够从 DataFrame 获取输入以及从该 DataFrame 中获取 X-Y 轴变量,使其在你想获得双变量分布,即两个变量的分布时非常有用。
例如,假设你想获取当考虑两个变量 Perimeter 和圆度时的数据点分布。你只需在 X 和 Y 参数中提到这些变量名称,如下所示。
sns.displot(df, x="Perimeter", y="roundness")
使用 Seaborn displot() 的双变量分布 | 图片作者
上述图表清楚地呈现出最大的数据点数量集中在 Perimeter 在 600 到 800 之间且圆度大于 0.85 的暗色区域。
这样你可以为所有数值列获得这种类型的双变量分布。
那么,分类列呢?
在数据集中,你可以看到有一个分类列 — Class,它表示不同的豆类。你可以使用这个变量来创建数据的子集,这些子集可以通过 displot
轻松绘制。
数据子集的图表
使用 displot()
函数时,你无需单独创建 DataFrame 的子集。你只需使用 **hue**
参数来为数据的每个子集创建直方图或 KDE 图。
让我们看看它的实际效果 —
sns.displot(df, x="Perimeter", hue='Class')
使用 Seaborn displot() 创建数据子集的直方图 | 图片作者
这就是你如何为数据的每个子集查看不同直方图的方法。
如果你希望在不同的子图上获取这些不同的直方图,那么你应该使用 col
参数,而不是 hue
参数,如下所示。
sns.displot(df, x="Perimeter", col='Class')
使用 Seaborn displot() 的子图 | 图片作者
这样,displot()
将根据你的不同子集创建相应数量的子图。
你可以根据项目需要探索 displot()
中的其他参数。
希望你觉得这篇文章有用。每个数据分析库随着时间的发展而不断演变。因此,一些函数会被弃用,取而代之的是改进后的函数,它们提供更好、更简单的用户体验。
尽管 Seaborn 中的 distplot()
已被弃用,但它并没有完全退出市场。你仍然可以使用它,但最好切换到更好的函数—displot
,以获得不同的分布图。
有兴趣阅读更多在 Medium 上的故事吗?
💡 考虑 成为 Medium 会员 以无限访问 Medium 上的故事和每日有趣的 Medium 通讯。我将获得你费用的一小部分,但对你没有额外费用。
💡 一定要注册并加入 100+其他人,以便不再错过关于数据科学指南、技巧和最佳实践的文章,包括 SQL 和 Python。
感谢阅读!
数据集:干豆数据集
引用:Dry Bean Dataset. (2020). UCI 机器学习库。 doi.org/10.24432/C50S4B
。
许可证:CC BY 4.0
AI 和数据领导力的最佳学习路径
原文:
towardsdatascience.com/the-best-learning-paths-for-ai-and-data-leadership-fabc3d4f8e36
让数据更有用
如何快速提升数据相关主题的能力
·发表于 Towards Data Science ·7 分钟阅读·2023 年 4 月 30 日
--
(觉得不耐烦?跳过文本和猫咪照片,直接查看学习路径!)
你的 作者。
首先,感谢大家鼓励我写作!我刚注意到,在 Medium 上,我的粉丝社区的规模是 奥巴马 的 70%。哇!
我被这个了不起的社区给予的所有爱所深感荣幸和谦逊。我不知道是因为我是一个乐观的怪人还是尽管如此,但谢谢你们!感谢你们对我职称的不响应,当我随机开关职称时,没有对任何指标产生影响——你们的存在对我来说意义重大,感谢你们关注我的想法,而不是我的标签。特别是自从我经历了近 10 年的职业变化,谁知道我最终决定的变化是否会像假期一样精彩。
与你分享有用的思考一直是我的极大快乐,但现在我已经发布了超过 180 篇博客文章,很多人告诉我你们在我的内容中感到困惑,需要更好的索引。结果发现,对于新读者来说,整理我写的各种主题是非常混乱的。我听到了!并非所有人都对所有内容感兴趣。最终,我会准备一个精心策划的网站来帮助你们,但在此期间,让我通过为所有文章添加标准化的超级标题来迈出第一步。这样,每次你都能知道自己在处理哪个类别,从而直接跳到你感兴趣的内容,而跳过我对随机深奥话题的思考。总的来说,这就像是我为你们准备了小型出版物供你们选择一样。
轻微偏离一下来捍卫各种话题的广泛性,在我脑海中,它们都与相同的事情有关:决策智能!* 无论写作内容有多么数据导向,它始终建立在提升现实世界行动的原则上。决策智能是关于给自己提供技能和工具,将信息(无论是你对午餐谈话的记忆还是你在庞大数据库中的探索)转化为更好的行动(决策!),无论规模(从小吃到千兆字节)还是环境(从选择大学专业到构建AI 系统)。我认为跨越这些话题范围是完全自然的——对于任何认真研究决策的学生来说甚至是必要的——尽管我承认,即使有 180+篇文章,我也只是勉强触及了所有值得了解的内容。
但如果你关注得更为狭窄,希望这个新的索引能为你的知识盛宴增添一些韵律和理由。
这是我的猫赫克斯利帮助我保持索引的地方。
决策技能
在这里,你会找到关于如何成为更好的决策者的建议,无论是否有复杂的算法。它专注于人类方面的内容,如与偏见作斗争、制定目标、理解你的非理性等。这是那些希望从心理学、经济学、神经科学、管理科学、谈判和其他经典决策科学等学科中获得智慧的人所需的地方。
示例:
数据驱动的领导力和职业
针对数据领导者和有志成为领导者的人。这是我放置关于组织中缺少什么、你可能做的导致数据团队成员离职的事情、按什么顺序招聘、如何建立数据驱动文化等文章的地方。我还从有志成为团队成员的角度包含数据科学职业文章,比如面试时需要问的问题… 对于经理来说也很有用(了解你的团队成员关于如何应对你的建议总是很有帮助)。
示例:
与 AI 交朋友
这里我以互联网有史以来最友好的方式,或者你(完全免费!)的钱退回,介绍关于 机器学习 和 AI 的概念。部分文章将是更深入(和讽刺的)探索,扩展我在 YouTube 上受欢迎的 与机器学习交朋友(MFML)课程 中的课程(索引 在这里),而其他文章则探讨 AI 时代精神 或我曾经被迫接受的 误解。在这里让自己免疫,以便这些对常识的侵犯不会再出现在你的口中。
示例:
不拘一格的揭秘者
我亲爱的 VC 和 CEO 群体,另请高就!(转到上述任何一个类别,但跳过这个。)这个是给(永远的)学生们的。你们中的一些人真的很喜欢我挑一个 随机的深奥术语 并详细解释它,使其变得直观。是的,这非常琐碎!是的,你们大多数人不关心它!但这些东西对那些,嗯,可能三个人来说就像猫薄荷一样,喜欢看 浮夸的术语被削弱、闪亮的新软件被挑剔直到它承认、以及 公式被解释 以便一个孩子(或高管)可以理解。因此,偶尔我会通过展示我们如何深入理解复杂事物来取悦我们四个人。如果我们深入了解,它们就会变得简单。这也是你会发现 为何一个主题在教科书中的位置 的地方。无论它应该在这里还是 绝对不应该在这里(即使没有人告诉学术界)。
示例:
统计思维
我是一个正从统计学中恢复过来的统计学家,可能永远不会完全恢复,因此我有很多要说的统计学内容。太多了!我在一个 10.5 小时的秘密课程中讲述了许多内容,关于统计决策的课程我还没有放到网上(前半小时有盗版形式,但大部分内容正在等待专业摄制组的录制——在此之前,唯一的观看方式是邀请我现场演讲)。偶尔,我会详细阐述课程中提到的一些内容,这个类别就是你可以找到这些内容的地方。
示例:
让数据变得有用
那些关注我一段时间的人应该会认识这三个词……“让数据变得有用的学科”是我对数据科学的定义。欢迎来到这个涵盖一般数据科学加上分析的类别,减去已经被吸收到上面更专业类别中的所有话题。如果你是一个实践中的数据科学家,你会想关注这个类别以及任何一个最符合你兴趣的前面类别。
示例:
如何黑客攻击自己
如果上述类别中没有,那么它要么是我在问答环节中给出的建议总结(通常涉及职业、生存、个人提升或生活平衡),要么是某种技能/见解,让我在成长为你们所熟知和喜爱的(或讨厌的,毕竟这是互联网,嗨)版本时变得更好一点。示例包括公众演讲技巧、制定新年决心的建议和数学冒名顶替症的想法。
示例:
哦,我文章中的许多链接将引导你到我写的其他与突出词相关的文章(还有其他链接会带你去隐藏彩蛋和幽默),因此我的博客是一个复杂的选择你冒险的网络。因为升级自己应该是有趣的,并且带有一点儿随意的偶然性。
享受吧!
(不要忘记 告诉我 你最感兴趣的类别,因为这将有助于塑造我选择的主题平衡。)
谢谢阅读!来看看课程如何?
如果你在这里玩得开心,并且你在寻找一个有趣的、以领导力为导向的课程,旨在让 AI 初学者和专家都感到愉悦,这是我为你准备的小礼物:
课程链接:bit.ly/funaicourse
阅读 Cassie Kozyrkov 的每一篇故事(以及 Medium 上的成千上万其他作家的故事)。你的会员费直接支持…
kozyrkov.medium.com](https://kozyrkov.medium.com/membership?source=post_page-----fabc3d4f8e36--------------------------------)
P.S. 你有没有尝试过在 Medium 上点击鼓掌按钮多次,看看会发生什么? ❤️
喜欢作者吗?与 Cassie Kozyrkov 联系
让我们成为朋友吧!你可以在Twitter、YouTube、Substack和LinkedIn找到我。想让我在你的活动中演讲?使用这个表单与我联系。
脚注
好吧,并不是所有*的内容;我承认那些教你关于公共演讲的课程确实是出于一时兴起。
人类开发者与 AI 合作伙伴的最佳结合
图片由作者使用 Midjourney 制作
生成性 AI 将如何影响产品工程团队 — 第六部分 | 后记
·
关注 发表在 Towards Data Science ·12 min read·2023 年 8 月 1 日
--
这是六部分系列的最后一部分,调查生成性 AI 生产力工具如何影响整个产品工程团队的结构,如 GitHub Copilot、ChatGPT 和 Amazon CodeWhisperer。
在最后一部分中,我们将探讨之前文章中忽略的许多复杂性,人工工程师在人工智能时代的不可替代价值,并对领导者提出反思、适应和创新的号召。
“以前在…”
在这一系列文章中,我们经历了不少旅程,我认为有必要简要回顾一下我们在前五篇文章中涵盖的内容,以便引入我们尚未涵盖的所有内容:
在第一部分中,我们看到生成式 AI 编码工具如 Copilot 和 ChatGPT 有潜力显著提升技术角色人员的生产力,如工程师和数据科学家。这些工具通过自动化繁琐、重复、耗时的任务,改变了开发人员需要做的工作。理论上,它去除了开发人员不喜欢的工作内容,使他们的宝贵工作变得更快更轻松。
在第二部分中,我们考虑到开发任务的自动化程度提高和开发人员生产力的提升,可能会导致产品团队中工程师与产品经理的比例发生重大变化;在历史上,团队中大约有五名工程师对应一名产品经理,但随着生成式 AI 工具的出现,这一比例可能会接近 1:1。
在第三部分中,我们探讨了生成式 AI 编码助手不仅具备了协助基本工作如重构代码或编写测试的能力。我们还探讨了长期来看,AI 工具可能会重构遗留代码,包括创建测试和文档,并简化一些团队结构,如移动应用开发,这些团队结构往往不是因为价值流的差异而创建,而是因为技术技能不同。我们还考虑了对初级工程师及其职业发展的可能影响。
在第四部分中,我们考虑了这种变化如何使公司能够在相同产出的情况下大幅减少工程师人数、在相同预算下快速增加产出,或某种组合。我们开始建模投资增长、预算维护或成本减少之间的选择可能对工程和产品人数的影响。
在第五部分中,我们调查了这些好处可能如何以不同的方式影响不同类型的公司。新的创业公司应该尽快开始使用这些新工具。较大的公司,虽然可能有最强烈的财务压力来改变团队结构,但由于文化惯性会面临适应困难,但今天应该鼓励他们开始实验这些工具,并根据自己的战略做出如何采用这些工具的决策。我们还看到,“外包”公司最有可能受到负面影响,因为客户希望保留内部员工并在其他地方节省成本。
迄今为止我们讨论的一切都围绕着假设“下一代产品工程团队可能会有更少的工程师”展开,但我认为可以公平地说,我们在证明或反驳这一假设方面还远远不够。
“是的,但……”
现在,我想讨论过去几个月我在这方面的对话中收到的许多发人深省的挑战,并且要讨论我在服务于最初假设时做出的大量假设。
我留下了很多潜在的反驳意见没有处理,不是因为它们没有困扰我,而是因为在实验这些工具并观察它们如何真正帮助团队的过程中,还有太多的不确定性和工作要做。
这是一个包含许多批评和未回答问题的大清单:
你把大量的信任寄托在 AI 工具上,希望它们能够编写优质的代码。它们不可能像人类一样优秀。
我承认,我对这些工具的质量感到印象深刻,但我也不试图证明它们需要像优秀开发者那样好。我故意避免建模没有技术专业知识的情境,因为根据我使用这些工具的经验,它们仍然需要大量的监督和指导。监督是为了确保它们不会做出愚蠢或危险的事情(比如将 API 密钥写入源代码),指导则是为了确保它们做对了事情(使用正确的语言、框架或设计模式)。
我有一种感觉,这些 AI 工具需要成为软件工程领域的麦当劳;虽然去一家优秀的非连锁餐厅与出色的员工合作可以是一种超凡的体验,但这并不总是可能的。麦当劳在全球范围内都是干净、便宜、健康(我的意思是没有细菌滋生)且最重要的是一致的。正如我一位亲爱的意大利朋友在面对一份大牌连锁披萨时所说,“它不会让你死”。在某种程度上,这就是我们对 AI 工具的期望。
但,我也不认为这就是故事的结局。我们今天看到的工具距离一年的质量还有很大差距。即便在我将原文编辑成这一系列文章的过程中,每天都有关于更多改进的消息;UC Berkeley 推出了一个比 GPT4 更擅长编写 API 调用的模型。微软宣布了“膨胀注意力”,允许模型扩展到数十亿个标记的 LongNet。StackOverflow 宣布了 OverflowAI,承诺对愚蠢的问题提供更多犀利的回应(抱歉,我的意思是,更好、更有用的搜索)。
即使你对现有工具的能力持怀疑态度,我担心忽视它们可能发展出的潜力会显得目光短浅。
[编辑:即便是在我第一次起草这篇文章的一周左右时间内,Stack Overflow 已经宣布了OverflowAI,Github 宣布了防止知识产权问题的额外工具,StabilityAI 宣布了一个专注于编码的 LLM。市场的发展速度令人惊叹]
AI 工具将会面临知识产权问题。还会有安全问题。如果它们停止工作,我们将无法继续工作。
是的,这些都是可能的。
但,这不是我们第一次处理类似问题,我们已经有了应对的方法。我与许多公司交流时发现,他们因为担心泄露公司机密而陷入某种程度的僵局,这些机密可能会被用来进一步训练 LLM。虽然对于免费和个人订阅用户,这种情况可能确实存在,但我强烈建议亲爱的读者自行研究大型供应商,了解具体的风险,以及这些供应商为解决这个非常合理的担忧所采取的措施。查看一些大公司的常见问题解答,以确定是否有足够好的答案来满足你的使用案例和风险情况:OpenAI,Github Copilot,AWS CodeWhisperer(Google Duet 仍在封闭测试中,数据安全文档尚未发布)。
安全和数据保护也是类似的情况。今天阅读这篇文章的大多数人已经依赖于 Github、微软或 AWS 的安全性。你可能将代码存储在 Github 中,或将应用程序或数据存储在 Azure、GCP 或 Amazon 上。问问自己,为什么你愿意接受超云供应商的风险,却不愿接受编码工具的风险。使用 ChatGPT 的风险不可忽视,5 月份报告了数据泄漏,本周报道的越狱潜力,以及云安全供应商 Netskope 报告的内部用户数据泄漏。和其他技术一样,你可以选择禁止它在你的组织中使用,但人们,像自然一样,总会找到办法。要妥善解决安全问题,你需要教育用户并提供安全、易于使用的替代方案。如果 OpenAI 无法胜任,也许其他供应商可以。
另一个担忧是无意中暴露知识产权风险,例如模型可能在‘(偶然?)’训练过程中使用了封闭源材料,这使得工具可能使你的组织面临违反法律的风险(并承担纠正违约的风险)。坏消息是——如果你认为这是一个新风险,你可能应该更仔细地检查你在组织中使用开源软件的情况。许多公司在正确管理和理解其“软件材料清单”(SBOM)——即其软件所依赖的封闭和开源源代码的列表——方面远远不够。你绝对应该担心这些工具中的一个可能会偶然使你侵犯他人的知识产权,但你应该扩展你已经用于开源软件和开发者的大红剪贴按钮的控制措施。
这些风险由你承担,如果你认真考虑这些工具可能提供的机会,你也应该阅读文档并与供应商讨论他们采取的保护措施。确保你做好功课。Copilot 的常识隐私报告给出的评分为 63%,较低,获得了‘黄灯’警告(它在‘学校’和‘家长同意’上的评分特别低,拖低了总分)。
这应该始终成为你采购流程的一部分。你始终需要从风险的角度考虑任何你打算放置在生产代码或数据旁边的工具,决定你对风险的接受程度以及如何减轻风险是你的责任。
更积极的一点是,我认为 Salesforce 最近的公告很好地指示了这些工具的发展方向。Salesforce 推动 AI Day 的主要营销重点之一是他们称之为‘Einstein Trust Layer’的部分,这似乎是一个真正令人印象深刻的包装,针对不同的 LLM 模型,能够大大增强访问安全性并保护客户和公司信息(说真的,看看这个视频,尽管它不是关于代码的)。
我们刚刚看到七家主要的科技公司(亚马逊、Anthropic、谷歌、Inflection、Meta、微软和 OpenAI)签署了自愿的‘负责任 AI’承诺,其中包括水印输出。可以合理地假设,这些市场上的最大玩家,大多数已经是我们信任的公司,将发布类似的信任和安全包装,以解决有关知识产权、安全、隐私以及 LLM 可能有些有毒和与事实关系可疑的问题。
仍然需要有人设计整体解决方案。
另见:
-
需要有人管理所有的数据和数据管道。
-
需要有人管理应用程序之间的重叠。
-
需要有人确保产品的安全性。
-
需要有人监控和回应问题。
-
需要有人管理团队和产品之间的依赖关系和互动。
对,你说得对。仍然有很多工作需要完成,不只是编写代码。
这太棒了!
这意味着我们仍然需要人类一段时间,并且开始显示我们应该为工程师提供培训的地方。但我们也应该预期工具、自动化和更高质量的代码会开始积极影响这些问题清单。通过更多的文档、减少‘聪明’的代码、更清晰的接口、更好的解释性和更好的测试覆盖,我们可能会看到这些挑战减少。
但工程师的大部分时间实际上并不是在编写代码,因为我们不断被要求去参加无聊的会议。
是的,没错。AI 并不会解决所有问题,但如果你作为工程师在会议上的时间比在编程上多,那么问题不在于你的生产力,而在于你所在组织的管理方式。
也许 AI 有一天会解决这个问题,但在短期内,更小、更精简和更自动化的组织可能不需要这么多会议。
坦率地说,对于大多数组织来说,提高开发者生产力的最佳方法是更好地优先排序工作,对低价值结果说不,并给团队更多时间专注于交付高质量的产品。但如果我们不能做到这一点,也许我们可以有 AI 编码助手来替代。
我们不能用 AI 来替代产品经理吗?
啊,这个很不错。
我收到的关于这些文章的一个令人惊讶的反馈是:“这真的很有趣,但在我的业务中,我们的产品支持远远不够。” 结果我发现,我用 5:1 的工程师比例展示了一个无意识的偏见,许多技术团队已经因为他们的比例不仅高得多(比如说 10:1 或更高),而且产品技能在组织中的价值不够高而面临巨大挑战。一些公司似乎仍然认为工程师写代码,而产品经理是一种昂贵的奢侈品。
我认为从客户那里引出需求是非常重要的。我也认为运行一个有效且易于理解的商业优先级流程是非常困难的。在我们能让利益相关者自己进行软件提示设计之前,还需要一段时间。
产品经理对出色的产品工程至关重要。产品经理集中工程资源,并帮助业务识别最有价值的结果。我认为我们现在最不需要的就是减少产品支持,即使我们可以通过自动化来协助他们的一些日常任务。
我怀疑在这些新团队中,产品经理和技术负责人将是最关键的角色。
你没有考虑 X、Y 或 Z
你几乎肯定是对的。不过如果你已经做到了,那就太好了!给我留言,开始讨论,或者最好的方式是写一篇新的文章,阐述你对 Y 或 Z 的看法。(但不要谈 X。我们不再讨论 X 了。)
让我们总结一下
这整个系列文章的重点是考虑一个问题:今天,我觉得还没有足够的产品和技术领导者积极参与讨论,如果生成性 AI 编码工具的承诺兑现,可能对他们的团队产生什么影响。我们不仅仅是在谈论对开发者生产力的小幅提升。我们可能谈论的是产品创建方式的改变。
如果我们讨论的内容有一部分是真的,那么构建软件的团队可能会与现在大相径庭,这将对产品和技术人员的预算产生重大影响。整个行业面临着破坏的风险,尤其是开发者角色的减少。
尽管公司可能会通过投资更多的成果来缓解这一问题,但一个企业可以支持的价值流是有限的。那些花时间仔细和果断地理解和接受即将到来的变化的公司可能会获得巨大的竞争优势。
这个论点也有哲学方面的内容。我相信人类工程师可能有一种不可言喻和不可替代的价值,但我不再能自信地告诉你这是什么了。
我们知道开发者编写代码,我们可以根据他们编写代码的速度和代码的有效性来为他们定价。但是这一说法未能真正理解工程师的实际工作是什么。多年来,很容易证明熟练的人类工程师能够做一些算法无法做到的事情,但现在情况不再如此。如果人类与机器之间的界限仅仅是可以创建的解决方案的复杂程度,那么我们正处于危险的沙土之中。正如我之前所写:
工程师不仅仅被教如何编程。他们被教如何思考。
作为一个行业,我们必须认识到这一代新工具可以轻松复制广泛的工程任务。在这样做的过程中,我们还应理解,仍然存在一些人类具备的独特技能是难以复制的。作为领导者,我们的工作是识别这些技能并投资于人员,以优化这些技能。
这篇文章是一个行动号召。如果你是产品或技术领导者,你应该将其视为一个挑战,考虑生成型 AI 工具对你们团队的影响,并让你的团队参与回答这个问题。
给定时间和空间来考虑这些工具将如何帮助他们,最有可能的创新来源将是你们的团队本身。但是,作为一个领导者,你也需要认识到,火鸡可能不太愿意为圣诞节投票。人们可能因为对就业的真实威胁感到害怕和不愿意参与,你需要发挥他们的才能来帮助你了解是否应该削减预算。成为老板很难,但否则你的竞争对手可能比你更果断和无情。
我们应该明确,最有趣、最混乱、最不受控制、最有影响力和最令人兴奋的进展将出现在那些小型初创公司和技术以外的团队中。不要犯错,忽视你自己组织中的非技术人员或初创社区中微小公司的贡献。
首先要对自己进行教育,然后对你自己公司里的其他人进行教育,了解真正的风险是什么。通过关注你公司内部的好奇早期采用者和外部技术巨头及小型初创公司的动向,考虑机会。要意识到这些进展可能会从根本上重塑你的产品工程,然后走出去,保持好奇,提问,并鼓励你的团队也这样做。
我还没有设法推翻自己的假设——但现在我需要你的帮助来设计实验。
本系列的其他文章:
附言:如果你喜欢这些关于团队的文章,看看我的Teamcraft 播客,在这里我和我的共同主持人安德鲁·麦克拉伦讨论了使团队成功的因素。
适合你神经网络的最佳优化算法
原文:
towardsdatascience.com/the-best-optimization-algorithm-for-your-neural-network-d16d87ef15cb
如何选择它并最小化你的神经网络训练时间。
·发表于 Towards Data Science ·13 分钟阅读·2023 年 10 月 14 日
--
图片来源:unsplash.com。
开发任何机器学习模型都涉及一个严格的实验过程,该过程遵循思想-实验-评估循环。
图片由作者提供。
上述循环重复多次,直到达到令人满意的性能水平。“实验”阶段包括机器学习模型的编码和训练步骤。由于模型变得更加复杂且在更大的数据集上训练,训练时间不可避免地增加。因此,训练大型深度神经网络可能会非常缓慢。
对于数据科学从业者来说,幸运的是,存在几种加速训练过程的技术,包括:
-
迁移学习。
-
权重初始化,如 Glorot 或 He 初始化。
-
批量归一化用于训练数据。
-
选择一个可靠的激活函数。
-
使用更快的优化器。
尽管我指出的所有技术都很重要,但在这篇文章中我将深入关注最后一点。我将描述多种神经网络参数优化算法,强调它们的优点和局限性。
在这篇文章的最后一部分,我将展示一个可视化图,显示讨论过的优化算法之间的比较。
对于实际实现,本文中使用的所有代码可以在这个GitHub 仓库中访问:
[## articles/NN-optimizer at main · andreoniriccardo/articles
通过在 GitHub 上创建一个帐户,参与 andreoniriccardo/articles 的开发。
批量梯度下降
传统上,批量梯度下降被认为是神经网络中优化方法的默认选择。
在神经网络对整个训练集X生成预测之后,我们将网络的预测与每个训练点的实际标签进行比较。这是为了计算一个成本函数J(W,b),它是模型生成准确预测能力的标量表示。梯度下降优化算法使用成本函数作为指导来调整每个网络参数。这个迭代过程持续进行,直到成本函数接近零,或无法进一步降低。梯度下降算法的作用是以特定方向调整网络的每个权重和偏置。选择的方向是最能降低成本函数的方向。
作者提供的图片。
从视觉角度来看,可以参考上面的图像。梯度下降从最左边的蓝点开始。通过分析起始点周围的成本函数梯度,它将参数(x 轴值)调整到右侧。这个过程重复多次,直到算法得到了非常好的最优近似。
如果成本函数有 2 个输入参数,函数不是一条直线,而是创建了一个三维表面。可以考虑该表面的等高线来显示批量梯度下降步骤:
作者提供的图片。
神经网络由成千上万的参数组成,这些参数会影响成本函数。虽然可视化百万维度的表示是不切实际的,但数学方程对理解梯度下降过程很有帮助。
首先,我们计算成本函数J(W,b),这是网络权重W和偏置b的函数。然后,反向传播算法计算成本函数相对于网络每层的每个权重和偏置的导数:
知道了调整参数的方向后,我们更新它们。每个参数更新的幅度由梯度本身和学习率 alpha 调节。Alpha 是优化算法的一个超参数,其值通常保持不变。
批量梯度下降提供了几个优点:
-
简洁性:这是一种直接且易于理解的方法。
-
超参数管理:只需要调整一个超参数,即学习率。
-
最优性:对于凸成本函数,它可靠地在合理的学习率下达到全局最优。
梯度下降的缺点是:
-
计算强度:它可能较慢,特别是对于大型训练集,因为在评估所有样本后更新参数。
-
局部最优和鞍点:它可能会陷入局部最优或鞍点,从而减慢收敛速度。
-
梯度问题:它容易出现梯度消失问题。
-
内存使用:它需要将整个训练数据存储在 CPU/GPU 内存中。
小批量梯度下降
在批量梯度下降算法中,所有训练样本都用于计算单个改进步骤。该步骤将是最准确的,因为它考虑了所有可用的信息。然而,这种方法在实际应用中往往过于缓慢,建议实现更快的替代方案。
最常见的解决方案是称为小批量梯度下降,它只使用训练数据的小子集来更新权重值。
考虑整个训练集 X 和相关标签 Y:
其中 m 表示训练样本的数量。
与其将整个批次喂给优化算法,不如仅处理训练集的一小部分。假设我们将子集 X^t 和 Y^t 喂给算法,每个子集包含 512 个训练样本:
例如,如果总训练集包含 5,120,000 个点,则可以将其分成 10,000 个小批量,每个小批量包含 512 个样本。
对于每个小批量,我们执行经典的梯度下降操作:
-
计算相对于小批量 t 的成本,J^t(W,b)。
-
执行反向传播以计算 J^t(W,b) 相对于每个权重和偏差的梯度。
-
更新参数。
下图中的绿色线条显示了小批量梯度下降的典型优化路径。虽然批量梯度下降会沿着更直接的路径到达最优点,但由于每次迭代中数据集的限制,小批量梯度下降似乎会采取几步不必要的路径。
图片由作者提供。
原因在于,小批量梯度下降在任何时间 t 只有一小部分训练集用于计算其决策。由于可用信息的限制,显然所走的路线不是最直接的。
然而,小批量梯度下降的显著优势在于每一步计算非常快速,因为算法只需评估数据的一小部分,而不是整个训练集。在我们的例子中,每一步只需要评估 512 个数据点,而不是 500 万。这也是为什么几乎没有真实应用需要大量数据时使用批量梯度下降的原因。
如图所示,使用小批量梯度下降算法无法保证迭代 t+1 的成本低于迭代 t 的成本,但如果问题定义良好,优化算法可以非常快速地达到接近最优点的区域。
小批量大小的选择代表了网络训练过程中的一个附加超参数。批量大小等于总样本数量 (m) 对应于批量梯度下降优化。相反,如果 m=1,我们正在执行随机梯度下降,每个训练样本就是一个小批量。
如果训练集很小,批量梯度下降可能是一个有效的选择,否则,像 32、64、128、256 和 512 这样的常见小批量大小通常被考虑。出于某种原因,等于 2 的幂的批量大小似乎表现更好。
虽然批量大小为 1 的随机梯度下降是一种选择,但它往往非常嘈杂,并且可能远离最小值。此外,考虑到每个样本的计算时间,随机梯度下降效率很低,因为它无法利用向量化的好处。
动量梯度下降
请记住,在批量和小批量梯度下降中,参数更新遵循一个定义的公式:
因此,在这个方程中,每一步优化的大小由学习率(一个固定的量)和在成本函数特定点计算出的梯度决定。
当梯度在成本函数的近似平坦区域计算时,它将非常小,从而导致相应的小梯度下降步长。考虑下图中点 A 和 B 的梯度差异。
图像由作者提供。
动量梯度下降 解决了这个问题。我们可以把动量梯度下降想象成一个沿着坡道滚下的保龄球,坡道的形状由成本函数定义。如果保龄球从坡道陡峭的部分开始下降,它的运动开始时较慢,但会很快获得速度和动量。由于其动量,即使在坡道的平坦区域,保龄球也能保持很高的速度。
这就是动量梯度下降的核心概念:算法会考虑以前的梯度,而不仅仅是迭代 t 时计算的梯度。类似于保龄球的类比,迭代 t 时计算的梯度定义了保龄球的加速度,而不是速度。
在每一步中,权重和偏置的速度都是使用前一速度和当前迭代的梯度计算的。
参数 beta,即动量,调节新速度值是根据当前斜率还是过去速度值来决定的。
最后,使用计算出的速度更新参数:
相较于迷你批量梯度下降,动量梯度下降在大多数应用中表现出更优越的性能。动量梯度下降相对于标准梯度下降的主要缺点是它需要额外的参数进行调整。然而,实践表明,beta 等于 0.9 的值效果很好。
RMS Prop
考虑一个成本函数,其形状类似于长形碗,其中最小点位于最窄的部分。该函数的轮廓由下图中的水平线条描述。
图片来源:作者。
在起始点距离最小值较远的情况下,梯度下降(即使是带有动量的变体)开始时沿着最陡的斜坡前进,在上图中,这不是通向最小值的最佳路径。RMS Prop优化算法的关键思想是早期修正方向,并更迅速地瞄准全局最小值。
与动量梯度下降类似,RMS Prop 需要通过一个额外的超参数(称为衰减率)进行调整。通过实际经验,已经证明将衰减率设置为 0.9 是大多数问题的良好选择。
Adam
Adam及其变体可能是神经网络中最常用的优化算法。Adam,即自适应动量估计,源自动量梯度下降和 RMS Prop 的组合。
作为两种优化方法的混合,Adam 需要调整两个额外的超参数(除了学习率 alpha)。我们称它们为 beta_1 和 beta_2,它们分别是动量和 RMS Prop 中使用的超参数。与其他讨论的算法一样,beta_1 和 beta_2 也有有效的默认值选择:beta_1 通常设置为 0.9,beta_2 设置为 0.999。Adam 还包括一个 epsilon 参数,它作为平滑项,并且几乎总是设置为像 e-7 或 e-8 这样的小值。
在大多数情况下,Adam 优化算法优于上述所有方法。唯一的例外是一些非常简单的问题,在这些问题中,简单的方法效果更快。Adam 的效率确实有一个权衡:需要调整两个额外的参数。然而,在我看来,这是为其效率付出的微小代价。
Nadam 和 AdaMax
值得提及的两个算法是作为广泛使用的 Adam 优化算法的修改版:Nadam 和 AdaMax。
Nadam,即 Nesterov 加速自适应矩估计,通过将 Nesterov 加速梯度(NAG)融入其框架来增强 Adam。这意味着 Nadam 不仅受益于 Adam 的自适应学习率和动量,还受益于 NAG 组件,这是一种帮助算法更准确预测下一步的技术。尤其在高维空间中,Nadam 的收敛速度更快,效果更佳。
AdaMax则采用了略微不同的方法。虽然 Adam 根据梯度的一阶矩和二阶矩计算自适应学习率,但 AdaMax 仅关注梯度的最大范数。AdaMax 在处理稀疏梯度方面的简单性和高效性使其成为训练深度神经网络的一个有吸引力的选择,尤其是在涉及稀疏数据的任务中。
优化算法比较
为了实际测试和可视化每种讨论的优化算法的性能,我使用上述每种优化器训练了一个简单的深度神经网络。网络的任务是对 28x28 像素的图像中的时尚物品进行分类。数据集名为 Fashion MNIST(MIT 许可),由 70,000 张小型灰度服装图像组成。
我用来测试不同优化器的算法在下面的代码片段中展示,更详细的信息请参见下方的 GitHub 仓库。
# Load data
(X_train, y_train), (X_test, y_test) = load_my_data()
# Print an example
#print_image(X_train, 42)
# Normalize the inputs
X_train = X_train / 255.
X_test = X_test / 255.
# Define the optimizers to test
my_optimizers = {"Mini-batch GD":tf.keras.optimizers.SGD(learning_rate = 0.001, momentum = 0.0),
"Momentum GD":tf.keras.optimizers.SGD(learning_rate = 0.001, momentum = 0.9),
"RMS Prop":tf.keras.optimizers.RMSprop(learning_rate = 0.001, rho = 0.9),
"Adam":tf.keras.optimizers.Adam(learning_rate = 0.001, beta_1 = 0.9, beta_2 = 0.999)
}
histories = {}
for optimizer_name, optimizer in my_optimizers.items():
# Define a neural network
my_network = tf.keras.models.Sequential([
tf.keras.layers.Flatten(input_shape=(28, 28)),
tf.keras.layers.Dense(128, activation='relu'),
tf.keras.layers.Dense(64, activation='relu'),
tf.keras.layers.Dense(10, activation='softmax')
])
# Compile the model
my_network.compile(optimizer=optimizer,
loss='sparse_categorical_crossentropy', # since labels are more than 2 and not one-hot-encoded
metrics=['accuracy'])
# Train the model
print('Training the model with optimizer {}'.format(optimizer_name))
histories[optimizer_name] = my_network.fit(X_train, y_train, epochs=50, validation_split=0.1, verbose=1)
# Plot learning curves
for optimizer_name, history in histories.items():
loss = history.history['loss']
epochs = range(1,len(loss)+1)
plt.plot(epochs, loss, label=optimizer_name)
plt.legend(loc="upper right")
plt.xlabel("Epoch")
plt.ylabel("Loss")
[## articles/NN-optimizer at main · andreoniriccardo/articles
通过在 GitHub 上创建账户来为 andreoniriccardo/articles 的开发做贡献。
结果图表在这条折线图中可视化:
图片由作者提供。
我们可以立即看到动量梯度下降(黄色线)比标准梯度下降(蓝色线)快得多。相反,RMS Prop(绿色线)似乎获得了与动量梯度下降相似的结果。这可能是由于几个原因,如超参数调节不完全或神经网络过于简单。最后,Adam 优化器(红色线)在所有其他方法中明显优越。
学习率衰减
我希望在这篇文章中包含学习率衰减,因为尽管它不严格算作优化算法,但它是一种加速网络学习过程的强大技术。
图片来源:unsplash.com。
学习率衰减指的是学习率超参数alpha在训练周期中的减少。这种调整是必要的,因为在优化的初始阶段,算法可以接受更大的步伐,但当它接近最小点时,我们更愿意采取较小的步伐,以便它能在更接近最小点的区域内跳跃。
有几种方法可以在迭代中减少学习率。一种方法由以下公式描述:
其中,衰减率是一个额外的调节参数。
另一种可能性是:
第一个叫做指数学习率衰减,而第二个中,参数k是一个常量。
最后,也可以应用离散学习率衰减,例如在每t次迭代后将其减半,或手动减少它。
图片由作者提供。
结论
时间对每个数据科学从业者来说都是宝贵而有限的资源。因此,掌握加速学习算法训练的工具可以带来差异。
在这篇文章中,我们已经看到标准的梯度下降优化器是过时的工具,并且存在一些替代方法,它们在更短的时间内提供更好的解决方案。
选择适合特定机器学习应用的优化器并不总是容易的。这取决于任务,而且尚未有明确的共识关于哪一种优化器最好。然而,正如我们上面所看到的,Adam 优化器在大多数情况下是一个有效的选择,了解最受欢迎的优化器的工作原理是一个很好的起点。
如果你喜欢这个故事,请考虑关注我,以便接收到我即将发布的项目和文章的通知!
这是我过去的一些项目:
了解像 Facebook 和 LinkedIn 这样的公司如何从网络中提取洞察。
语言模型能否发明独特的幻想角色名字?让我们从零开始构建它。
使用深度学习生成幻想角色名称:从头构建语言模型 [## 使用 Scikit-Learn 的支持向量机:友好的介绍
每个数据科学家都应该在工具箱中拥有 SVM。了解如何通过实践掌握这一多用途模型…
参考文献
-
深入学习 — Aston Zhang、Zachary C. Lipton、Mu Li 和 Alexander J. Smola
-
深入了解整流器:超越 ImageNet 分类的人类水平表现 — Kaiming He、Xiangyu Zhang、Shaoqing Ren、Jian Sun
-
动手机器学习:使用 Scikit-Learn、Keras 和 TensorFlow(第 2 版) — Aurélien Géron
PyCon US 2023 最佳讲座
原文:
towardsdatascience.com/the-best-talks-from-pycon-us-2023-91d470ba5265
我们对世界上最大 Python 大会的看法
·发表于 Towards Data Science ·11 分钟阅读·2023 年 4 月 25 日
--
PyCon 2023 标志。图片由 PyCon US 2023 提供
从 2023 年 4 月 19 日至 4 月 23 日,我们参加了第二届 PyCon —— 这是 Python 编程语言的最大年度大会。每年,PyCon 在全球举办多个会议,我们参加了在犹他州盐湖城举行的美国会议。
今年,PyCon 庆祝了其 20 周年,在开幕式上,回顾视频展示了之前参会者分享的过去会议的照片和记忆。虽然这是我们第二次参加 PyCon,但我们很高兴重新联系了去年结识的朋友,并建立了新的联系。总的来说,我们很享受这次会议,并想回顾一些我们喜欢的数据科学相关讲座。
如果你无法参加 PyCon,也不用担心!PyCon 计划将讲座上传到他们的 YouTube 频道供大家观看。我们会在讲座上线后在这里提供链接。
下面是我们将介绍的讲座的快速概述:
-
特征工程是为每个人准备的!— Leah Berg 和 Ray McLendon
-
Ned Batchelder 的主旨演讲
-
为什么你应该关注开源供应链安全 — Nina Zakharenko
-
基于 LLM 的代理:如何使用 Haystack 开发智能 NLP 驱动的应用程序 — Tuana Celik
-
进入 Logisticverse:使用 Python 提高交通网络效率 — Uzoma Nicholas Muoh
-
生成器、协程和纳米服务 — Reuven M. Lerner
-
公共数据讨论
-
自然语言处理中的公平性和偏见缓解方法 — Angana Borah
-
10 种测试中可能犯的错误 — Shai Geva
-
Python 与 UX 的结合:通过代码提升用户体验 — Neeraj Pandey, Aashka Dhebar
特征工程是为每个人准备的!— Leah Berg 和 Ray McLendon
在会议演讲开始的前两天,PyCon 提供了多种不同主题的教程。这些教程是与会者学习一个主题并通过 Python 实际应用的绝佳方式。
去年我们举办了一次自然语言处理研讨会后,能够再次被选中教授一个关于 Python 特征工程的三小时初学者研讨会,我们感到非常兴奋。通常,特征工程仅在为机器学习模型创建输入的背景下讨论。然而,我们抓住机会解释了它如何也能增强数据可视化、数据质量和可解释性。
在整个研讨会中,我们介绍了如何探索和创建离散数据和连续数据的特征。与会者通过在 Google Colab 笔记本中分析技术产品评论和股票市场数据获得了实际经验。
现在,让我们深入探讨一下我们在会议中最喜欢的一些演讲。
主题演讲 — Ned Batchelder
Ned Batchelder,波士顿 Python 组织者,coverage.py 维护者,以及 edX 的贡献者,在会议的第一天以软件开发人员沟通的重要性开场。
虽然许多会议与会者可能对这次非技术性的演讲感到惊讶,但我们发现它非常清新且急需。沟通是任何协作环境中的一个重要组成部分,而 Ned 在技术会议上强调这一点,提醒我们在这个领域中它的价值。
由 Priscilla Du Preez 拍摄,Unsplash 提供
Ned 的演讲突出了一个重要观点:每条信息,无论是面对面交流、通过文本、电话还是其他媒介传达,都承载着信息和情感。无论你的意图如何,人们会根据各种因素(包括他们与你的历史、与你的相似性或他们当前的情感状态)来解读你的信息情感。
Ned 提出的改善沟通的建议包括实践谦逊、明确表达以及谨慎选择用词。随着他分享了不良沟通的例子以及如何改进,我们不禁反思自己的沟通风格。这是一个发人深省的提醒,让我们意识到我们都可能曾经发送或接收过被误解的信息,我们都可以从努力更有效地沟通中受益。
为什么你应该关心开源供应链安全 — Nina Zakharenko
开源供应链。图片由 SLSA 提供
许多数据科学家依赖开源 Python 模块,如 pandas、numpy 和 scikit-learn 来进行他们的工作。然而,容易忽视这些包可能存在的潜在风险,包括它们如何成为攻击的目标以及这可能产生的影响。如果你像我们一样,可能以前没有考虑过这些问题,但了解它们对确保你的工作安全和完整性至关重要。
Nina 的演讲对供应链每个步骤中的各种攻击进行了出色的概述,并提供了这些攻击的最新实例。
-
未经授权的更改 — 攻击者修复了一个漏洞,同时引入了另一个漏洞(例如:Linux 伪善者提交)
-
被攻击的源代码仓库 — 攻击者获得对私人系统的访问权限并对源代码进行恶意修改(例如:PHP 自托管 Git 服务器)
-
从修改后的源代码构建 — 攻击者从一个与官方仓库不匹配的版本构建源代码(例如:Webmin)
-
被攻击的构建过程 — 攻击者获得对构建系统的访问权限并注入恶意代码(例如:SolarWinds)
-
被攻击的依赖 — 攻击者攻击了广泛使用的依赖项以访问其依赖项(例如:event-stream)
-
上传修改后的包 — 攻击者获得对系统的访问权限并可以上传修改后的包(例如:Codecov)
-
被攻击的包仓库 — 攻击者攻击了整个包仓库(例如:Browserify 伪造攻击)
-
依赖变得不可用 — 许多其他包依赖的包不再可用(例如:left-pad)
Nina 通过分享一个名为 Scorecard 的工具来结束了演讲,该工具帮助开源维护者评估他们项目的安全状态。
基于 LLM 的智能体构建:如何使用 Haystack 开发智能 NLP 驱动的应用程序 — Tuana Celik
Deepset 是一家拥有搜索引擎背景的欧洲公司。他们将生成预训练变换器(GPT)模型集成到他们的工作中,并创建了一个名为 Haystack 的开源库。该库让你可以快速轻松地构建一个用于简单问答(QA)系统的“智能体”。
图片由 Jon Tyson 提供,来源于 Unsplash。
尽管 Haystack 可以应用于 QA 系统之外,但演讲特别集中在这一用例上。他们首先解释了 Haystack 如何将文档转化为向量或“嵌入”,这使得在进行查询时可以快速从存储中提取相关文档。通过这种方法,代理可以用类似于 ChatGPT 的自然语言回答查询。
使用 Hugging Face 上现有的模型是 Haystack 的一大优势。即使是较简单的模型也可以在无需完整 GPT-4 风格模型的情况下提供优异的结果。通过微调这些模型,性能可以进一步提升,从而创建出世界一流的 QA 系统。
使用这些技术,可以开发一个完全自包含的系统,达到 GPT 级别的性能,而不必使用第三方 API 来冒险泄露组织的数据。
进入物流宇宙:使用 Python 改善运输网络的效率 — Uzoma Nicholas Muoh
完全披露,Nick 是我们的朋友,我们在 PyCon 2022 上认识了他,我们非常喜欢他今年的演讲。Nick 深入探讨了创建高效运输系统的复杂世界。
Nick 首先讨论了运输公司、司机和产品公司不同的视角。高效运输货物的挑战在于平衡产品运输需求、司机休息时间以及减少空载运输的里程。
图片由 Sven Brandsma 提供,来源于 Unsplash。
我们很喜欢 Nick 介绍了我们之前没有使用过的几种 Python 库,例如 Google 的 ortools。这让我们深刻认识到,我们试图通过数据科学解决的许多问题,已经被其他学科(如运筹学)处理过了。
演讲还包含了使用网络图可视化货物运输位置的精彩部分。这使得识别低效路线变得容易,并找到优化整体运输过程的方法。看到这些可视化如何用于解决物流中的复杂问题,真是非常有帮助。
生成器、协程和纳米服务 — Reuven M. Lerner
作为数据科学家,我们曾从一些软件开发者朋友那里简要了解过生成器,但坦率地说,我们并没有多加使用。我们很高兴从流行的 Python 教育者和作者 Reuven 那里学到了更多关于它们的知识。
生成器允许你即时迭代一系列值,而不是一次性生成所有值。它们非常适合迭代大型数据集,因为它们在内存使用上非常高效,因为它们不会将所有值存储在内存中。数据科学家可以使用生成器读取大型数据集或生成无限的数字序列。
协程可以消耗和生成值。协程可用于创建轻量级的并发任务,这些任务可以暂停和恢复执行,以允许其他任务运行。这对于网络通信或输入/输出操作等任务很有用,因为在这些任务中,切换任务而不是阻塞程序直到任务完成是有益的。
最后,Reuven 介绍了纳米服务的概念,这可以被认为是坐在程序内并通过 API(即send())访问的协程,类似于微服务如何被划分为小部分,每部分都有自己的服务器和状态。这种方法允许在代码中实现更大的模块化和灵活性,因为你可以在需要时访问这些小部分或“纳米服务”。
公民数据讨论
PyCon 的一个独特方面是无法在线访问或在活动结束后获取的开放空间。这些是指定的房间,参与者可以聚集在一起讨论感兴趣的话题。
我们参加了一个专门讨论公民数据的开放空间,很高兴看到一群多样化的个人分享他们独特的观点。我们从讨论中得到了一些关键的启示。
首先,我们发现每个辖区都有自己的公民数据获取政策。虽然这些数据是公开的,但获取这些数据并不总是直接的。有些辖区要求数据必须亲自获取或在实体 CD 上获取,这对那些寻求数据的人来说可能带来挑战。
Chris Yates在Unsplash上的照片
讨论的第二点是,第三方承包商通常会聚合数据,但合同不总是要求数据集必须是开放的。因此,以前免费的但难以获取的数据可能变得昂贵,这让我们感到惊讶。
最后,我们了解了美国数字服务(USDS),这是一个致力于提升美国数字服务标准的政府组织。我们很高兴见到一些 USDS 成员,并听到他们为改变合同、使公民数据更开放而做出的精彩工作。
自然语言处理中的公平性与偏见缓解方法 — Angana Borah
作为自然语言处理(NLP)领域的专业人士,我们对公平性和偏见话题有强烈兴趣。这次讲座很好地涵盖了这个主题,从高层概述到对用于开发更好 NLP 解决方案的标准指标和技术的深入探索。
Angana 对她所做和将继续做的工作的热情在讲座中显而易见。她强调了在自然语言处理(NLP)中解决公平性和偏见问题的重要性,以及当前训练方法的不足。进一步的资金和研究是必要的,以开发更好的系统来解决这些问题。
对于这个话题的深入了解,我们推荐 Michael Kearns 和 Aaron Roth 的《伦理算法》。Angana 在讲解公平性和偏见方面做得很出色,但这本书进一步扩展了隐私等话题,这在当前被广泛使用的大型语言模型(LLMs)中是一个主要关注点。
10 种自我挖坑的测试方法 — Shai Geva
数据科学家常常缺乏软件开发背景,这可能导致忽视最佳实践,例如编写测试。Shai 的演讲提供了一个很好的介绍,阐述了不仅要编写测试,还要有效编写测试的重要性。他的演讲涵盖了 10 个糟糕测试的标志。
图片来源:Karl Pawlowicz 由 Unsplash 提供
-
没有测试 — 如果你还没有为你的项目编写测试,先从小而简单的测试开始。
-
如果不失败,就不算通过 — 始终确保你的测试以你预期的方式失败。
-
测试过多内容 — 每个测试应确认代码行为的一个单一事实。
-
语言不明确 — 在测试的名称中使用决定性、明确和具体的语言。
-
细节决定成败 — 尝试将所有信息孤立在测试本身,而不是在代码中跳来跳去。
-
测试未隔离 — 你不希望测试结果会因运行顺序的不同而变化。
-
范围不当 — 尝试使用一致的行为测试。
-
到处都是测试替身 — 尽量不要使用 mock、patch 等,因为对代码库的更改会迅速使其过时。
-
测试慢 — 目标是三秒内完成测试,并在观察模式下运行,以便快速识别和解决瓶颈。
-
错误的优先级 — 测试的目标是发现 bug。它们应该具备以下特性:可维护、快速和强大。
我们喜欢 Shai 演讲的简洁性,并且在下次编写测试时会牢记这些原则。
Python 与用户体验:通过代码提升用户体验 — Neeraj Pandey, Aashka Dhebar
Neeraj 的讲座突出了用户体验和数据科学之间的协同作用。用户体验(UX)设计师和研究人员通常会进行各种实验,收集有关设计选择和/或用户行为的数据。使用 Python 的数据科学技术可以帮助分析这些数据。
Neeraj 关于鞋店的案例研究完美地展示了这些协同作用。他演示了如何使用 k-means 聚类来个性化推荐、促销和内容,以及如何使用自然语言处理(NLP)技术如情感分析从客户反馈中识别用户痛点。
点击流分析用于优化网站导航和提高用户参与度,而市场篮分析帮助调整商店布局,以便根据经常购买的商品进行调整。Neeraj 还展示了如何使用 NLP 技术自动化用户研究调查结果,以及如何使用统计技术如假设检验来评估 A/B 测试结果的显著性。
图片由 Firmbee.com 提供,来源于 Unsplash
总体而言,他的讲座突出了数据科学如何帮助改善 UX 并推动业务成功。尽管他讨论的技术对我们来说并不新鲜,但我们欣赏他清晰而引人入胜的演讲风格以及精美制作的幻灯片。
我们之前已经与公司 UX 部门分享了一些这些技术,但我们相信 Neeraj 的案例研究将进一步强化我们一直试图传达的观点。我们很高兴与 UX 的同事分享他的演讲。
结论
我们在 PyCon 上进行的第二次工作坊非常愉快,并收到了有价值的反馈,这将帮助我们改进。我们很高兴将这些反馈融入到工作坊的扩展版本中,在这个版本中我们将教授我们的数据科学流程,帮助你提高项目的成功率(更多内容请见 这里)。
虽然我们觉得今年的数据科学讲座没有去年那么多,但我们仍然在面向一般软件开发的会议中发现了价值。
如果你是一名数据科学家,正在寻找一个 Python 会议参加,我们推荐这个会议,如果你有兴趣成为一个更全面的程序员并学习更多关于软件开发的知识。无论你的技能水平如何,社区都很欢迎你,这也是一个扩展网络的绝佳机会。
偏差-方差权衡
理解这一著名概念及其对机器学习模型的影响
·
查看 发表在 数据科学的前沿 ·5 分钟阅读·2023 年 2 月 23 日
--
图片由 Piret Ilver 提供,来源于 Unsplash
偏差-方差权衡是数据科学领域中的一个基础且广泛讨论的概念。理解偏差-方差权衡对于开发准确和可靠的机器学习模型至关重要,因为它可以帮助我们优化模型性能,避免如欠拟合和过拟合等常见问题。
在定义偏差-方差权衡之前,需要先分别定义偏差和方差。
偏差与方差
偏差
偏差指的是通过使用简化模型来逼近实际问题而引入的错误。具有高偏差的模型无法捕捉数据的真实复杂性,容易发生欠拟合,导致在训练数据和测试数据上表现不佳。偏差通过目标变量的期望值或真实值与模型预测值之间的差异来表示。
方差
方差是指模型对训练数据中小的波动的敏感性引入的错误。具有高方差的模型倾向于对训练数据过拟合,从而在新的、未见过的数据上表现不佳。方差通过模型对不同训练集的预测结果的变异程度来表示。
理解偏差-方差权衡对于开发准确可靠的机器学习模型至关重要。它有助于优化模型性能并避免诸如欠拟合和过拟合等常见陷阱。通过像下图这样的靶心图来可视化偏差和方差的概念是一种有效的方法。
来源:V. Gudivada, A. Apon & J. Ding, 2017
图示展示了方差和偏差的关系:
-
一个具有高偏差和高方差的模型会产生大量错误,并且结果非常不一致。
-
一个具有高方差和低偏差的模型往往更准确,但结果会有很大的变动。
-
一个具有高偏差和低方差的模型会产生许多错误预测,但结果非常一致。
-
最后,一个具有低偏差和低方差的模型能做出良好的预测,并且结果一致。
从图中可以直观地看到,所有模型应该具有低偏差和低方差,因为这种组合会产生最佳结果。然而,这就是偏差-方差权衡出现的地方。
权衡
图片来源:Possessed Photography 在 Unsplash
偏差-方差权衡出现的原因是增加模型的复杂性可以减少偏差,但会增加方差。另一方面,降低复杂性可以减少方差,但会增加偏差。目标是找到偏差和方差之间的最佳平衡,从而在新的、未见过的数据上实现最佳的泛化性能。
这直接与所使用模型的复杂性有关,如下图所示。
偏差-方差权衡和误差关系(图像由作者提供)
图表显示了模型复杂性如何与偏差和方差的值相关。低复杂度的模型可能过于简单,无法理解训练数据中的模式,这种现象称为欠拟合。因此,它在测试数据上无法做出良好的预测,导致高偏差。
另一方面,具有过多自由度的模型可能导致所谓的过拟合,即模型在训练数据上表现优秀,但在测试数据上的性能显著下降。这是因为模型对训练数据过于适应,丧失了泛化能力,当需要解释从未见过的数据样本时,无法得到良好的结果。
随着模型复杂性的增加,偏差减少(模型对训练数据的拟合更好),但方差增加(模型对训练数据的敏感度更高)。最佳的权衡点出现在误差最小的位置,在这种情况下,是在适中的复杂度水平。
为了帮助理解,我们来看一个实际的例子,说明偏差-方差权衡的概念。
例子
Steve Johnson 的照片,来源于 Unsplash
为了说明偏差-方差权衡在机器学习模型中的影响,让我们看看具有不同复杂度水平的模型在相同数据集上的训练和测试表现。
对于这个例子,将生成一个输入 X
和输出 y
之间具有二次关系的随机数据集。然后,我们将数据分成训练集和测试集,并拟合三个不同度数的多项式回归模型(1、2 和 20)。我们将结果模型与训练数据和测试数据一起绘制,并计算训练集和测试集的均方误差。
结果图显示了不同多项式回归模型的偏差-方差权衡:
不同度数模型得到的结果(图像由作者提供)。
模型的度数为 1 时过于简单,具有高偏差和低方差,导致欠拟合,并在训练数据和测试数据上都出现高误差。模型的度数为 20 时过于复杂,具有低偏差和高方差,导致过拟合,在训练数据上误差低,但在测试数据上误差高。模型的度数为 2 时在偏差和方差之间有一个良好的平衡,结果是测试误差最低。
这个例子演示了找到机器学习模型的合适复杂度水平的重要性,以平衡偏差和方差,并在新的、未见过的数据上实现良好的泛化性能。
希望这篇文章能够帮助你理解偏差-方差权衡,以及在开发机器学习模型时如何考虑它。
任何评论和建议都非常欢迎。
随时通过我的 LinkedIn 联系我,并查看我的 GitHub。
偏差-方差权衡,解析
原文:
towardsdatascience.com/the-bias-variance-tradeoff-explained-2d1311c2b7c2
颠覆性的揭示者
偏差-方差权衡,第三部分,共 3 部分
·发布于 Towards Data Science ·4 分钟阅读·2023 年 2 月 15 日
--
我们在这一系列的第一部分和第二部分中涵盖了许多内容。第一部分是开胃菜,我们讨论了理解偏差-方差权衡旅程中需要了解的一些基础知识。第二部分是我们丰盛的主菜,我们深入探讨了过拟合、欠拟合和正则化等概念。
吃蔬菜是个好主意,因此在继续之前,最好去阅读那些早期的文章,因为第三部分是甜点:你通过遵循逻辑所获得的总结。
我们的总结将一目了然。图像由作者提供。
偏差-方差权衡一目了然
偏差-方差权衡的核心是:
-
训练性能和实际性能(你关心的那个)并不相同。
-
训练性能是关于模型在其学习的旧数据上的表现,而你真正关心的是当你输入全新数据时,模型的表现如何。
-
当你增加复杂性以提高过拟合而不改善实际性能时,当你将模型应用于验证集时会发生什么?(或者如果你像冠军一样使用四分法,可以应用于调试集)。你会看到标准差(方差的平方根)增长的速度超过了偏差的缩小速度。你在训练中使事情变得更好,但在整体上却变得更糟!
-
当你减少复杂性以提高你的欠拟合而不改善真实表现时,当你将模型应用到你的验证集(或调试集)时会发生什么?你会看到偏差比标准差缩小得更多。你在训练中让事情变得更好,但在一般情况下变得更糟!
-
黄金模型就是你不能在不比标准差更多地损害偏差的情况下改善偏差,反之亦然。那就是你应该停下的地方。你已经把事情做好了!
这张图是一个卡通草图,对于挑剔的数学家来说不够通用,但它传达了要点。由作者创建。
长话短说:偏差-方差权衡是一个有用的方式来思考调整正则化超参数(这是一个复杂的词,意为“在拟合模型之前必须选择的设置”)。最重要的是,有办法找到复杂度的最佳点!这涉及在你改变正则化设置时观察调试数据集中的均方误差(MSE)。但如果你不打算这样做,你可能最好忘掉你刚刚读到的一切,记住这点:
不要试图作弊。你无法超越你的信息能买到的最佳模型。
不要试图作弊。如果你的信息不完美,那么你对任务的建模能力是有上限的。你可以在你的训练集上做得“更好”,但在(适当大小的)测试集或现实世界中做不到。
所以,停止认真对待训练性能结果,学会像成年人一样验证和测试。(我甚至为你写了一个简单的以 Mr. Bean 为主角的解释,这样你就没有借口了。)
如果你理解数据拆分的重要性,你可以忘掉整个讨论。
说实话,那些理解数据拆分重要性的人(以及模型的真正考验是其在未见过的数据上的表现)大多数可以忽略这整个讨论,继续过自己的生活。
换句话说,除非你打算调整正则化模型,否则如果你的逐步流程在应用 ML/AI 方面是稳固的,著名的偏差-方差权衡你不需要了解太多。只需避免本指南中的AI 白痴的不良行为,你就会没事的。
7 种严重搞砸你的机器学习项目的方法
towardsdatascience.com
感谢阅读!怎么样来个课程?
如果你在这里玩得开心,并且在寻找一个设计来取悦 AI 初学者和专家的有趣领导力课程,这是我为你准备的小东西:
课程链接:bit.ly/funaicourse
想要提升决策技能而不是提升你的 AI 能力?你可以通过 这个链接 从我这里学习决策智能课程。
[## 你生活的方向盘——决策智能视频教程 | LinkedIn Learning…
决策能力是你能学到的最宝贵的技能。你的人生归结为两件事:你的生活质量…
bit.ly](https://bit.ly/decisioncourse?source=post_page-----2d1311c2b7c2--------------------------------)
附言:你有没有试过在 Medium 上多次点击拍手按钮看看会发生什么? ❤️
喜欢这位作者?与 Cassie Kozyrkov 连接
让我们成为朋友吧!你可以在 Twitter、YouTube、Substack 和 LinkedIn 上找到我。想让我在你的活动上发言?请使用 这个表单 与我联系。
阅读 Cassie Kozyrkov 的每一个故事(以及 Medium 上其他成千上万的作者的故事)。你的会员费用直接支持…
kozyrkov.medium.com](https://kozyrkov.medium.com/membership?source=post_page-----2d1311c2b7c2--------------------------------)
数据科学的诞生:历史上首次假设检验与 Python 见解
深入了解每位数据科学家都需要掌握的 Python 驱动的见解
·发布于Towards Data Science ·7 分钟阅读·2023 年 9 月 8 日
--
对图片选择感到困惑?通过阅读文章发现原因!图片来源:pixabay.com.
在 18 世纪伦敦精致的氛围中,一位开创性的个体开始了一项任务,这将永远改变我们对数据和统计学的理解。约翰·阿布斯诺特,一位杰出的苏格兰医生和数学家,踏上了一段非凡的旅程,驱动他的则是对出生记录复杂性的无法抑制的好奇心。他未曾意识到,他的好奇心将为统计学的革命奠定基础。
男孩出生是否比女孩多?
这是 18 世纪让约翰·阿布斯诺特感兴趣的直接问题。他想了解为什么似乎出生的男婴比女婴多。他的好奇心促使他分析了多年来来自伦敦的大量出生记录。实质上,他试图弄清楚这种模式是否自然或随机,或者是否存在更深层次的解释来说明男性和女性出生数量的差异。
数据收集
阿布斯诺特的数据收集工作非常卓越。在 1629 年至 1710 年的几十年间,他收集了伦敦的出生数据。这些记录提供了丰富且可靠的数据来源,涵盖了相当大一部分人口。
阿布斯诺特对收集这些历史出生数据的奉献为他后来的分析奠定了基础。这些广泛的记录为他提供了调查出生性别比例趋势的机会,为他开创性的统计分析奠定了基础。
零假设
阿布斯诺特对出生性别比例的自然研究是他研究的核心。他假设在人口中,男性出生与女性出生的比例应该大致相等。换句话说,他认为长期来看,性别之间不应存在显著偏倚。
这一假设基于“自然”平衡男女后代数量的观点,如果这种平衡被打破,可能对物种的生存和延续产生更广泛的影响。阿布斯诺特的工作是对现代人口性别比例理解和偏离预期比例研究的先驱。
阿布斯诺特对这一自然假设的好奇心使他踏上了以数据驱动的旅程,最终完成了他著名的分析和开创性的可视化工作,我们将在本文中进一步探讨。
统计分析与可视化
既然我们已经深入了解了约翰·阿布斯诺特(John Arbuthnot)杰出的数据收集工作,现在让我们关注他在 18 世纪如何进行统计分析和数据可视化。我们还将探索如何使用现代 Python 模拟来重现他的工作。本文中提到的所有代码可以在我的GitHub库中自由访问:
[## GitHub - andreoniriccardo/articles
通过在 GitHub 上创建账户,参与 andreoniriccardo/articles 的开发。
github.com](https://github.com/andreoniriccardo/articles?source=post_page-----4745dccaf6d--------------------------------)
作者提供的图片
阿布斯诺特的开创性贡献之一是他巧妙地使用了数据可视化。在没有现代统计软件的情况下,他创建了一个时间序列图,生动地表现了男性出生数量超过女性的年度情况。这种可视化帮助他揭示了数据中的模式和趋势。
作者提供的图片
从图表中可以看到,阿布斯诺特在水平轴上表示每一年,在垂直轴上表示每年的男性出生超出女性出生的数量。图表类似于过山车,起伏反映了出生性别比例的年度波动。
阿布斯诺特对这幅图的解释至关重要。他注意到,尽管有起伏的波动,但没有明显的长期趋势。换句话说,虽然男性和女性出生比例每年有所变化,但它似乎始终高于他认为的预期或自然的 1:1 性别比例(虚线)。这一时期的平均值为每 100 个女孩出生 107 个男孩,最高达到每 100 个女孩出生 116 个男孩。
为了更详细地理解这一点,考虑一下如果男性和女性出生数量相等,每年的过剩应为零。任何偏离零的情况都表明一种性别的过剩。
在现代背景下,我们可以使用 Python 重现阿布斯诺特的分析。通过绘制相同的数据并应用统计技术,我们可以验证他的发现,并展示今天数据可视化和分析工具的强大功能。绘制上面图表的代码如下:
阿布斯诺特的见解和遗产
阿布斯诺特的开创性实验标志着假设检验的诞生。除了定义自然假设的出生比例相等外,他还进行了统计假设检验,计算了我们现在称之为 p 值(使用符号检验),将其解释为统计显著性的度量,并大胆地拒绝了零假设。
他最初的假设很简单:如果零假设成立,即男性和女性出生的数量相等,那么这种明显的男性出生过剩必须不仅仅是巧合。他计算出这种情况纯属偶然的概率是极其微小的。为了说明这一点,可以考虑这个类比。我们可以将性别比例的结果等同于连续 82 次掷硬币,每次都得到正面。发生这种事件的概率极其小,82 次连续得到正面的概率为 1/2⁸²。这个极其微小的数字表明零假设——男孩和女孩的出生数量相等——是不可信的。
阿布斯诺特不仅揭示了他发现的统计不可能性,还进一步探索。他提出了一种潜在的力量在起作用,导致男性出生的过剩。他假设这种力量旨在抵消男性观察到的更高死亡率,暗示了一种“天意”的存在。这一可能的解释在现代理论中已被否定。
阿布斯诺特的研究,像任何科学努力一样,也不是没有缺陷。他的数据可能存在计数错误,而且他的分析仅限于英国国教徒的洗礼。然而,尽管存在这些局限性,他的核心发现经受住了时间的考验。今天,普遍接受的“自然”性别比例大约是每 100 个女孩出生 105 个男孩,这验证了阿布斯诺特的基本见解。
现代解释:Python 模拟
在本节中,我们将利用现代统计工具的能力重新审视 Arbuthnot 的假设并对其进行检验。通过 Python 模拟,我们将探讨观察到的男性出生过剩是否确实是偶然的结果,还是有更深层的原因。所有代码都可以在我的 GitHub 仓库 中找到:
## GitHub - andreoniriccardo/articles
通过在 GitHub 上创建一个账户来为 andreoniriccardo/articles 的开发做贡献。
如果男孩与女孩的出生比例恰好为 50:50,我们可以将这一过程看作是一次公平的掷硬币。通过几行代码,我们可以模拟掷 82 枚硬币(如同 Arbuthnot 考虑的年份数)几百万次的过程:
由于无处不在的 中心极限定理,我们可以自信地说,结果分布遵循正态分布:
作者提供的图片。
我们可以清楚地看到,如预期的那样,在 82 次掷币中,预期值为 41 次正面,获得超过 60 次正面/反面的概率是微不足道的。
我们可以进一步突出一些百分位。我选择绘制 97.5 和 2.5 百分位,因为它们代表了在显著性水平为 5% 的双尾假设检验中的接受限度。
作者提供的图片。
任何超过 50 或低于 32 的结果都被认为是统计学上显著的证据,以拒绝零假设。
最后,借助于 scipy 库,我们可以进行正式的 z 检验:
输出是:
结论
在重新审视 John Arbuthnot 令人瞩目的 18 世纪数据分析时,我们不仅向一位统计学先驱致敬,还挖掘了现代数据科学的根源。Arbuthnot 的显著性检验照亮了假设检验和统计推断在我们解读数据底层意义中的持久重要性。未来的数据科学家们,请记住;Arbuthnot 的开创精神提醒我们,即使在过去,统计洞察也为未来铺平了道路,使数据驱动的发现继续塑造我们对世界的理解。
如果你喜欢这个故事,考虑关注我以便获得我即将发布的项目和文章的通知!
这里是我过去的一些项目:
利用遗传算法和 Google Maps API 探索欧洲 50 个最受欢迎城市的魅力,开启高效的旅行体验…
## 从头开始使用 Numpy 构建深度神经网络 ## 欧罗巴旅行优化:遗传算法和 Google Maps API 解决旅行推销员问题
现代深度学习库是强大的工具,但它们可能使从业者对神经网络的…视而不见。
## 欧罗巴旅行优化:遗传算法和 Google Maps API 解决旅行推销员问题
参考资料
-
J. Arbuthnot, ‘论神圣天意的辩护…’,《哲学学报》第 27 期(1710 年),186-190 页。
Decent 机器上的 Pump & Dump 浓缩咖啡配置的诞生
咖啡数据科学
低流量的挑战
·
关注 发表在Towards Data Science ·5 分钟阅读·2023 年 1 月 24 日
--
我在使用 Decent Espresso Machine 时的一个挫折是水分配器。它导致低流量、高温度情况下的萃取偏向左侧,这对 staccato 浓缩咖啡特别具有挑战性。因此,我决定改进我的配置,以便再次制作 staccato 咖啡。
使用过的咖啡
我不想浪费大量咖啡,所以我的调查从使用过的咖啡开始。
我能否将水均匀地推送到 staccato 咖啡的底层?
我用 Fellow Shimy 筛选了干的、用过的咖啡渣,并用底部 6 克细磨和顶部 13 克粗磨制作了 staccato shots。
第一次测试运行了我的 Vaporexpress 配置文件 4 秒。咖啡被蒸了,但触摸时不湿。
我拿着这咖啡重新制作了相同的 shot,但我运行了 9 秒。然而,它速度很快,咖啡出来时非常湿。我怀疑是因为咖啡已经被蒸过。
我用干的、用过的咖啡渣重新进行了 6.5 秒的测试,但结果与第一次测试相同。
我接着进行了 10 秒,但有一侧出现了通道。
然后我尝试了一个双重双重和一些延长的预浸泡。
无流量
我查看了蒸汽 PI 然后没有流量(0 ml/s 流量)。我考虑将其作为脉冲动作进行。结果很有趣,似乎更均匀。
压力没有下降,我询问了 DE Diaspora。我被告知尝试将压力设置为 0 bar,这样可以排出水。
我用真正的咖啡迭代了几次,并将排水时间缩短至 5 秒。我不知道在蒸汽预浸泡期间的最佳泵送和排水次数。
然后我将结束的斜坡改为两个斜坡,中间有一个排水。
我还明确测量了废水,以了解用了多少水。我惊讶地发现水的 TDS 只有 0.05%,这意味着废水中咖啡含量非常少。
我使用了 DE 的输入估算和这些测量数据来估算因蒸汽损失了多少水。
设备/技术
浓缩咖啡机: 优质浓缩咖啡机
咖啡研磨机: Niche Zero
咖啡: 自家烘焙咖啡, 中度(第一次爆裂+1 分钟)
备料: Staccato 压实
过滤篮: Wafo Classic
其他设备: Atago TDS 计, Acaia Pyxis 秤
性能指标
我使用了两组指标来评估技术之间的差异:最终评分和咖啡提取。
最终评分是基于 7 个指标(Sharp、Rich、Syrup、Sweet、Sour、Bitter 和 Aftertaste)的评分卡的平均值。这些评分当然是主观的,但它们是根据我的口味进行校准的,并帮助我改进我的咖啡制作。这些评分有一些变化。我的目标是每个指标的一致性,但有时颗粒度很难把握。
溶解固体总量(TDS)是使用折射计测量的,这个数字与咖啡的输出重量和输入重量结合,用于确定杯中提取的咖啡百分比,称为提取率(EY)。
强度半径(IR)被定义为 TDS 与 EY 的控制图上原点的半径,因此 IR = sqrt( TDS² + EY²)。这个指标有助于在输出产量或冲泡比例之间标准化咖啡制作表现。
可行性数据
在这些拍摄中,味道有了显著的提升,TDS/EY/IR 也有所改进。这一档案还需要更多的工作,但这些结果给了我很大的希望。
理论
我对蒸汽的理论是,蒸汽将可溶物推向咖啡饼底部。因此,当咖啡饼顶部的水排出时,水中几乎没有可溶物。由于蒸汽的作用,可溶物会卡在咖啡饼的更深处。这就是为什么多次泵与倾倒不会将可溶物从咖啡中去除到废水中的原因。
我完全意识到这个档案看起来非常疯狂,特别是因为它与典型的 9 bar 浓缩咖啡非常不同。这个档案的压力甚至没有达到 2 bar,但提取效率简直疯狂。这个档案让我对达到 Kim Express 的表现甚至更高的表现充满希望。
未来充满希望,泵与倾倒!
如果你愿意,可以关注我的 Twitter、YouTube 和 Instagram,我会在这些平台上发布不同机器的意式浓缩咖啡制作视频和咖啡相关的内容。你也可以在 LinkedIn 找到我。你还可以关注我的 Medium 和 订阅。
我的进一步阅读:
捕捉-重捕方法
在不进行完全统计的情况下估算人群大小
·
关注 发表在Towards Data Science ·7 min read·2023 年 9 月 13 日
--
当你捕捉我们的个体时,确保不要伤害它们,因为你还需要将它们放回去。照片由Anne Nygård拍摄,来自Unsplash
在这篇文章中,我想介绍一种统计方法来估算一个人群的大小而无需完全统计,这种方法被称为捕捉-重捕方法。该方法源于生物学领域,但也可以应用于许多其他领域和场景,对数据科学家及相关职业可能具有重要意义。
我将首先在生物学示例中演示这一过程,然后再谈论其统计背景和使用所需的属性。之后,我将介绍一些不同领域的示例,以展示捕捉-重捕方法在不同场景中的能力。
我花园里有多少只蜗牛?
许多人不喜欢蜗牛,但我仍然认为它们很可爱。让我们在不伤害它们的情况下计算它们的数量。照片来自 Krzysztof Niewolny 在 Unsplash
假设我想知道我花园里有多少只蜗牛。我可以尝试统计它们的数量,但我怎么知道何时完成?即使我找不到更多蜗牛,我也不能确定没有剩下的蜗牛。相反,我可以使用另一种方法。
在第一天,我花半小时收集和计算蜗牛。此外,我在每只蜗牛上涂上一个油漆点,然后再放回我的花园。假设我收集了 21 只蜗牛。我能否估算我花园里蜗牛的总数?不,还不能(除了必须至少有 21 只蜗牛),但我还没完成。
一天后,我再次到我的花园开始计算蜗牛,时间为半小时。我发现的一些蜗牛已经在壳上有油漆点,即我昨天已经找到了它们,而其他蜗牛没有(即我昨天没有找到那个特定的蜗牛)。假设我那天计算了 28 只蜗牛,其中 9 只已经标记了油漆点。现在我可以估算蜗牛的总数。让我们来算一下。
在第二天,我发现的蜗牛中有 9/28 的比例是前一天已经找到的。这个比例应该等于我在第一天发现的蜗牛数与总蜗牛数的比例,即 21/N = 9/28,其中 N 是蜗牛的总数。我可以重新计算得到蜗牛总数 N = (21*28)/9 = 65。
为什么会这样?在第二天,某个比例的个体(比如 p%)具有某种属性(即被标记)。如果我从总体中随机抽样,我期望样本中 p% 也具有该属性。这是非常直观的:如果你从你所在城市的人口中随机抽样,你也会期望样本中的性别比例反映总体中的性别比例,对吧?然而,在第二天我们知道了这个比例 p,而在第一天我们不知道(当第一天画蜗牛时,我们不知道已经捕获了多少蜗牛),所以在第一天我们标记了 p% 的所有蜗牛。现在很容易从中推导出总蜗牛数:如果我标记了 21 只蜗牛,而我现在知道这占总体的 9/28=32%,那么总共有大约 65 只蜗牛(21 只大约是 65 的 32%)。
捕捉重获的条件
在使用捕捉-再捕捉方法之前,请确保满足所需的条件。照片由崇镇·赵拍摄,来自Unsplash
除了计算花园中蜗牛的数量,还有许多其他场景可以应用上述方法。正如你所想,两个采样步骤之间的间隔不一定要是一天,并且标记的方式也不一定要以字面上的标记个体来进行。你还可以仅仅保持一个你在第一次采样中抽取的个体的列表,只要你能轻松确定在第二次迭代中发现的个体是否已经在列表上。然而,为了使捕捉-再捕捉方法适用,必须满足以下一些条件:
-
在两个数据收集点上,人口必须相同。特别是,这要求在两个时间点之间没有个体被添加或移除。
-
在两个数据收集点上,都必须从分布中随机且独立地抽取。即每个个体被捕获的可能性必须相同。特别是,是否被标记不应影响在其他场合被抽取的可能性。
-
每次抽取的个体数量必须足够大,以产生有意义的重叠。你可以很容易地想象,从你的本地图书馆中随机抽取 100 本书,而图书数量达到数百万,这样的抽取根本没有重叠,因此对你的估计没有帮助。
示例用例
亮点:医学是一个经常使用捕捉-再捕捉方法变体的领域。照片由克谢尼娅·雅科夫列娃拍摄,来自Unsplash
现在我们已经理解了捕捉-再捕捉方法,让我们看看一些使用它的例子。它在我们想要确定一个人口的规模但无法完全计数时特别有用。然而,不同的场景可能会有不同的方法先决条件的陷阱需要考虑。
计算派对上的宾客数量
在你参加的下一次聚会上,你可以花五分钟标记一些个体(无论是通过字面标记还是保持一个名单),然后几分钟后再次随机抽取一些个体。然而,要确保你真的随机且独立地抽取。也就是说,你应该从各个地方捕捉人群,不要对认识或不认识的人有偏见。此外,确保两个数据收集点之间的距离不要太大,否则你的估计可能会因为人们在这段时间离开聚会而产生偏差。
从两个独立列表中进行捕捉
捕捉-重捕法的一种变体不在不同时间点进行重捕,而是使用两个独立的数据源(这些数据源来自相同的分布)及其重叠部分。这样,该方法通常用于医学场景中,因此我们来看一个例子,估算一种疾病的流行程度。
假设我有一份来自医院的患者名单,列出了 142 名患有某种疾病的人员,还有一份来自国家卫生服务的名单,列出了 442 名患有该疾病的人员。假设 71 个人出现在这两个名单上。那么我们可以使用上述公式得到我们的结果(142*442)/71 = 884。即估计有 884 人患有这种疾病。
对于这种变体,最重要的是这两个列表确实是独立的。即,一个个体属于一个列表的可能性不应因该个体是否属于另一个列表而有所不同,反之亦然。
估算潜在客户的数量
假设你有一个网站来销售你令人惊叹的新产品。某一天你捕捉了网站上所有访客的数据(例如,通过跟踪他们的 IP),几天后你再次进行相同的捕捉。通过两天之间的重叠部分,你可以估算你的产品的潜在客户数量。然而,你应该意识到这种情况可能容易违反一个重要的假设,即两次捕捉的独立性。特别是,可以认为第一次访问网站可能会增加再次访问网站的可能性。
总结
我们现在已经看到了一些捕捉-重捕法的示例,该方法允许我们在不完全计数的情况下估算一个种群的规模。该方法要求从种群中抽取两个独立的样本(可以在不同的时间点或来自不同的来源),并利用它们的重叠部分来估计种群的大小。这种方法可以在多种领域中使用,特别是当对种群的全面观察不可行时。
进一步阅读
关于在花园里计数蜗牛的例子,我改编自以下书籍:
- Kit Yates (2019). 《生命与死亡的数学:为什么数学(几乎)是所有的一切》。Quercus Editions Ltd, London.
关于在医学领域使用的捕捉-重捕法的概述可以在这里找到:
- Ramos, P. L., Sousa, I., Santana, R., Morgan, W. H., Gordon, K., Crewe, J., … & Macedo, A. F. (2020). 捕捉-重捕方法及其在眼科和视觉科学中的可能性综述。眼科流行病学,27(4),310–324。
喜欢这篇文章吗? 关注我 以便收到我未来的帖子通知。
GPT-4 的碳足迹
原文:
towardsdatascience.com/the-carbon-footprint-of-gpt-4-d6c676eb21ae
最近泄露的数据使我们第一次能够估算训练 OpenAI 的 GPT-4 的碳排放
·发布于 Towards Data Science ·阅读时间 7 分钟·2023 年 7 月 18 日
--
照片由 Taylor Vick 提供,来源于 Unsplash
随着最近的新闻警告我们全球平均温度持续上升 [1],我们需要提醒自己,大多数人类活动都有碳足迹,这对全球变暖和其他气候变化有贡献。这对于数字技术尤其是 AI 也是如此。本文作为一个提醒,估算了训练 OpenAI 的大型语言模型 GPT-4 的碳排放。
为了进行这些估算,我们需要了解:
-
训练 GPT-4 使用了多少电力
-
电力的碳强度,即生成 1 千瓦时电力的碳足迹
让我们直接进入主题。
[## 使用我的推荐链接加入 Medium - Kasper Groes Albin Ludvigsen
作为 Medium 会员,您的一部分会员费用将分配给您阅读的作者,并且您可以完全访问所有故事……
kaspergroesludvigsen.medium.com
GPT-4 的电力消耗
首先让我们估算 GPT-4 的能耗。根据未经验证的信息泄露,GPT-4 在大约 25,000 个 Nvidia A100 GPU 上训练了 90-100 天 [2]。
假设这些 GPU 安装在 Nvidia HGX 服务器中,每台服务器可以容纳 8 个 GPU,这意味着需要 25,000 / 8 = 3,125 台服务器。
估算电力消耗的一种方法是考虑 Nvidia HGX 服务器的热设计功率 (TDP)。TDP 以瓦特为单位,表示硬件在最大理论负载下的功耗 [11],即实际功耗可能有所不同。
不幸的是,Nvidia 并没有披露这些信息,因此我们改为使用类似 Nvidia DGX 服务器的 TDP,6.5 kW [3]。因此,如果一台 Nvidia DGX 服务器以满功率运行 1 小时,它将根据 TDP 消耗 6.5 KWh。
记住,估计训练 GPT-4 需要 90–100 天。这是 90 或 100 * 24 = 2,160 到 2,600 小时每台服务器。如果我们假设服务器始终以满功率运行,我们可以将小时数乘以 6.5 kW,因此在训练期间,每台服务器可能消耗了 14,040 到 16,900 KWh 的电力。
我们将其乘以 3,125 台服务器所需的 25,000 GPU:3,125 * 14,040 到 16,900 KWh = 43,875,000 到 52,812,500 KWh。
在计算计算机硬件的能耗时,通常需要将硬件的能耗乘以硬件运行的数据中心的所谓能效比(PUE)(参见例如 [4])。PUE 是一个描述计算机数据中心使用能源效率的比率。假设 GPT-4 是在微软 Azure 数据中心进行训练的,因为 OpenAI 与微软有合作关系。微软 Azure 数据中心的平均 PUE 为 1.18 [7],但请注意,这在不同数据中心之间可能有所不同。
因此,我们将 43,875,000 到 52,812,500 KWh 的硬件电力消耗乘以 1.18。这样我们得到 51,772,500 到 62,318,750 KWh。即训练 GPT-4 可能使用了 51,772,500 到 62,318,750 KWh 的电力。
这结束了我们对 GPT-4 训练能耗的估计。现在让我们估算训练 GPT-4 的碳足迹。
ChatGPT 可能消耗了相当于 2023 年 1 月 175,000 人的电力。
towardsdatascience.com
GPT-4 的碳足迹
上述估计 GPT-4 的训练电力消耗在 51,772,500 和 62,318,750 KWh 之间。
要将其转换为碳足迹,我们需要将其乘以用于提供计算电力的电力的碳强度。
我想强调的是,我们不知道电力的生成方式,因此不知道其碳强度,但我们可以做一些假设。
假设 GPT-4 是在 Azure 数据中心进行训练的,因为 OpenAI 与微软有合作关系。根据研究人员的说法,美国最低碳强度的 Azure 数据中心位于加利福尼亚州的 West US [5]。该数据中心的碳足迹为 240.6 gCO2e/KWh,这意味着在该地区生成 1 KWh 的电力平均会排放 240.6 克 CO2e。
因此,如果模型是在加利福尼亚州的“普通”电网电力上进行训练的,我们可以估算 GPT-4 的碳足迹在 12,456 和 14,994 公吨 CO2e 之间。
如果 OpenAI 在加拿大东部地区的数据中心训练 GPT-4,该地区的碳强度仅为 20 gCO2e/KWh,是所有 Azure 地区中最低的,那么碳足迹将为 1,035 到 1,246 公吨 CO2e。下图 1 显示了这一差异。
图 1:如果 GPT-4 是在 Azure 云区域加拿大东部进行训练的,其训练碳足迹将比原来小 13 倍。
讨论
本文估计,训练 GPT-4 可能排放了超过 15 公吨的 CO2e。这相当于 938 名美国人的年排放量[8]。或者占全球排放量的 0.0000375%,假设全球年排放量为 400 亿吨[9]。虽然这可能不算多,但与硬件制造排放和为大规模用户群体服务模型的碳足迹相比,就显得微不足道了。这一点我在我的文章中有提到过,无处不在的生成性 AI 的环境影响 和 ChatGPT 的电力消耗**.
上述内容中,我估计 GPT-4 的电力消耗在 51,772,500 到 62,318,750 KWh 之间。相比之下,估计训练 GPT-3 的电力消耗为 1,287,000 KWh[6],如图 2 所示。因此,如果这里的估算是正确的,那么训练 GPT-4 所需的电力可能是训练 GPT-3 所需电力的 40 至 48 倍,尽管 GPT-4 的总参数数量大约是 GPT-3 的 10 倍。显然,训练模型所需的电力还受到许多其他因素的影响,而不仅仅是模型的参数数量,但这提醒我们,不能仅凭类似模型的能耗来推测其他模型的能耗。
图 2:比较训练 GPT-3 和 GPT-4 的估计电力消耗
上述结果表明,Azure 云区域之间的碳排放可能相差 13 倍。这清楚地显示了在绿色能源地区训练模型的巨大环境效益 —— 这是我在这里也提到的:
有两种简单的方法来估算机器学习模型的碳足迹,并提供了 17 个减少碳足迹的想法。
towardsdatascience.com
此外,请注意,这里获得的结果基于关于用于训练 GPT-4 的 GPU 数量的未验证信息。因此——以及因为对 PUE、硬件利用率和碳强度等假设——这里获得的结果应被视为有根据的猜测。如果这些未验证的信息被证实为真实,我相信这里提供的数字大致是正确的,但请质疑我的假设。
结论
本文估算训练 GPT-4 消耗了 51,772,500 到 62,318,750 千瓦时的电力,并排放了 12,456 到 14,994 公吨 CO2e(如果在加利福尼亚州训练)和 1,035 到 1,246 公吨 CO2e(如果在加拿大东部训练)。
尽管这些数字可能看起来很小,但与 AI 模型生命周期其他阶段的环境影响(例如部署阶段)相比,它们显得微不足道。
这里提供的碳足迹估算可以供有兴趣计算 AI 全球总碳足迹的组织或个人使用。
另一个有趣的发现是,估算结果清楚地显示了考虑到模型训练云区域电力碳强度的环境效益。在比较 GPT-4 和 GPT-3 的电力消耗时,我们还发现它们的电力消耗差异远大于它们的大小差异。
如果数十亿人开始每天使用生成式 AI 技术,我们的环境可能会发生什么变化?
[towardsdatascience.com
就这些!希望你喜欢这个故事。让我知道你的想法!
通过注册 Medium 会员此处来获得 Medium 的福利并支持我的写作。
关注我以获取更多有关 AI 和可持续性的内容,并订阅以在我发布时通过电子邮件接收我的故事。
我有时也会写有关时间序列预测的文章。
也可以在LinkedIn上随时联系我。
参考文献
[1] edition.cnn.com/2023/07/05/world/hottest-day-world-climate-el-nino-intl/index.html
[2] archive.md/2RQ8X
[3] www.nvidia.com/content/dam/en-zz/Solutions/Data-Center/nvidia-dgx-a100-datasheet.pdf
[5] github.com/mlco2/impact/blob/master/data/impact.csv
– MIT 许可证 – “在此特此免费授予任何个人” [10]
[6] arxiv.org/ftp/arxiv/papers/2204/2204.05149.pdf
[8] https://www.forbes.com/sites/robtoews/2020/06/17/deep-learnings-climate-change-problem/
[9] https://www.iea.org/reports/co2-emissions-in-2022
[10] https://github.com/mlco2/impact/blob/master/LICENSE
[11] https://www.intel.com/content/www/us/en/support/articles/000055611/processors.html
反对人工智能监管的理由毫无意义
原文:
towardsdatascience.com/the-case-against-ai-regulation-makes-no-sense-a6b5a9ca2b28
意见
欧洲正在走在正确的道路上——世界其他地区应该效仿
·发布在 Towards Data Science ·阅读时间 9 分钟·2023 年 8 月 12 日
--
由作者通过 MidJourney 创建
自从 OpenAI 在 2022 年底推出 ChatGPT 以来,世界一直在热议生成式人工智能及其可能创造的未来。资本主义的拥趸们认为这项技术是一种净积极因素;数字世界的逻辑延续,它促成了无数财富的创造…… 只为少数人。与此同时,怀疑者回顾 80 年代的最佳科幻作品,并担忧我们可能正在创造自己的HAL / SHODAN / Ultron / SkyNet / GLaDOS。
这些只是声音响亮的少数人。大多数人面对生成式人工智能提供的可能性时,理解这项技术仅仅是一个工具,没有自己的思想。责任在于用户去“做好事”。如果这不可能,因为“好”本质上是主观的……那么民主政府需要介入并进行监管。
如何(以及是否)实施这一点仍在激烈讨论。欧盟是第一个推出提议的《人工智能法》的。虽然这只是一个不完美的初稿,但它具有管理高度颠覆性技术的真实尝试的优势,而不是让科技亿万富翁来决定。以下是提议法律的概要,以及这种监管的利弊。
欧盟《人工智能法》包含哪些内容。
《人工智能法》将风险置于讨论的核心:“新规则根据人工智能的风险水平为提供者和用户设立了义务。尽管许多人工智能系统的风险很小,但仍需进行评估。”
-
AI 产生“不可接受”风险(行为操控、实时和远程生物识别、社会评分……)的情况将被禁止。
-
高风险人工智能系统(涉及执法、教育、移民……)“在投入市场前和在整个生命周期中都将进行评估”。
-
限制风险的人工智能系统需要“遵守最低透明度要求,以便用户做出明智的决定。”
在提议的法规中,生成性人工智能得到了特别提及。使用该技术的公司将需要:
-
公开人工智能生成的内容。
-
设计保障措施以防止生成非法内容。
-
发布用于培训的版权数据的摘要。
如果这看起来满足实际需求同时又不至于过于宽泛,听从你的直觉。未能遵守规定的公司可能面临高达年营业额 6%的罚款,并被禁止在欧盟运营。该地区预计占全球人工智能市场的 20%到 25%,该市场预计在 10 年内将价值超过 1.3 万亿美元……这就是为什么科技公司可能会说他们会离开……但永远不会。该法律预计将在 2024 年左右通过。
为什么生成性人工智能不应受到监管。
关于科技亿万富翁表示 他们希望人工智能受到监管的说法已有很多讨论。让我们澄清一点:这只是一个幌子,纯粹是公关策略。他们并不真正想要监管,如果真的有,他们希望按自己的条件来。以下是他们及其小分队在过去几个月提出的一些最佳论据。
扼杀创新与进步
有人认为,法规将减缓人工智能的进展和突破。不允许公司进行测试和学习将会使它们在国际上竞争力下降。然而,我们还没有看到确凿的证据证明这一点。即使有,问题仍然存在:无限制的创新是否对整个社会有利?利润不是一切。也许欧盟在创造新的独角兽和亿万富翁方面会落后于中国和美国。如果我们仍然拥有社会保障、免费医疗、育儿假和每年 6 周的假期,这是否真的那么糟糕?如果因为法规的原因导致一个百万富翁无法成为亿万富翁,那也无所谓。
非国际竞争力的论点对于当前讨论更具相关性:法规可能为新公司设置进入壁垒(高成本、标准或对开发者或用户的要求),从而增强现有企业的优势。欧盟在实施 GDPR时已经看到过这种情况。法规需要为非常小的公司留出实验空间,这一点已经在欧盟层面讨论过。如果它们这么小,鉴于 AI 的指数级力量,中小企业能造成多大伤害呢?
复杂且具有挑战性的实施
与改变世界的技术相关的法规通常可能过于模糊或宽泛,难以适用于不同的司法管辖区。这使得它们在不同领域的实施和执行变得困难。特别是在考虑到该领域缺乏明确标准时,这一点尤为明显。毕竟,什么是风险和伦理,除了文化相对主义之外,还能是什么呢?
这使得平衡国际标准和主权成为一个特别棘手的话题。人工智能跨越国界运作,其监管需要国际合作与协调。考虑到不同的法律框架和文化差异,这可能会很复杂。这就是他们会说的。
然而,呼吁全球统一监管的声音却很少。人工智能(在许多方面)并不等同于原子弹,无论灾难论者如何呼吁“新 START”方法。欧盟将有自己的法律,其他世界大国也将如此。我们唯一能要求的,是围绕技术带来的风险达成共识,并有限度地合作以覆盖区域法律中的盲点。
过度监管和意外后果的潜力
此外,我们知道监管往往无法跟上技术的快速变化。人工智能是一个迅速发展的领域,新的技术和应用不断出现。新挑战、风险和机会不断出现,我们需要保持足够的敏捷性/灵活性来应对这些变化。跟上技术进步并监管尖端技术对管理机构来说可能具有挑战性……但这从未阻止过任何人,世界依然存在。
同时,政府必须确保新兴产业(不被视为人工智能)不会被现有法规的范围所涵盖,导致意外后果。例如,我们不希望因为碳捕捉系统使用类似生成性人工智能的技术来推荐清理区域,而使生态环境遭受损害。
避免过度的官僚主义和繁文缛节非常重要……但这并不是无所作为的理由。欧盟提出的基于风险的治理方案是应对这些挑战的良好答案。风险的定义足够全面,适用于所有人,同时允许在人工智能的性质发生变化时进行调整。
事实上,监管人工智能的真正风险很少……而好处却很多。
为什么生成性人工智能需要监管
有许多理由需要监管生成性人工智能,特别是从对弱势或无助人群的风险角度来看。当你从未遭受过歧视时,自动化和大规模歧视可能很容易被忽视……看看你,科技兄弟们。
确保人工智能的伦理使用
首先(而且显而易见),需要规制以将现有的数字法律应用并适应 AI 技术。这意味着要保护用户的隐私(以及他们的数据)。AI 公司在处理数据密集型算法时应投资于强大的网络安全能力……并放弃一些收入,因为用户数据不应出售给第三方。这是一个美国公司似乎固有地 且故意误解的概念,如果没有规制的话。
正如 AI 法案中提到的,科技公司还必须消除处理敏感话题的算法中的潜在偏见和歧视。这意味着 A) 确保没有被故意注入的偏见,B) 确保去除自然发生的偏见,以避免大规模再现。这是不可谈判的,如果需要规制碰撞测试,也无妨。
从哲学上讲,规制可以帮助在生成式 AI 的用户、开发者和利益相关者之间培养信任、透明度和问责制。通过让所有参与者披露 AI 输出的来源、目的和局限性,我们将能够做出更好的选择……并信任他人的选择。社会的结构需要这个。
维护人权和安全
除了“基础知识”之外,规制需要保护广大人群免受与 AI 相关的安全风险,这些风险有很多。
大多数风险将与人类有关。恶意行为者可以利用生成式 AI 来传播虚假信息或创建深度伪造。这很容易做到,而且公司似乎无法自己阻止——主要是因为他们不愿意(而非不能)标记 AI 生成的内容。我们的下一次选举可能取决于规制的实施……而(主要是年轻的)女性在全球范围内可能会问为什么我们不早点做。
我们还需要避免让人类利用生成式人工智能对其他人造成身体伤害:有报道称,人工智能可以用来描述制造脏弹的最佳方法。在这里,如果一个公司无法尽其所能地防止这种情况,我看不到我们继续允许它以现有形式存在的理由。
所有这些还不包括人工智能驱动的战争和自主武器,它们的创造必须尽一切可能避免。然而,这种情景极其灾难性,我们常常用它来掩盖人工智能的许多其他问题。为什么要集中注意数据隐私呢,当终结者就在眼前,不是吗?不要让悲观者分散你对非常乏味但却非常真实的事实的注意:如果没有强有力的人工智能监管来解决上述问题,社会可能会死于千刀万剐,而不是单一的武器打击。
这就是为什么我们必须确保公司同意创建符合人类价值观和道德的系统。说起来容易做起来难,但有一个愿景是一个好的开始。
减轻社会和经济影响
有些重要话题《人工智能法案》(或任何其他提议的法规)并没有完全涵盖。这些问题将在未来几年进一步评估,但其本质使得在不进行过度监管的情况下进行监管变得困难,但这并不减少其必要性。
首先,需要规则来公平补偿那些数据被使用以训练将给少数人带来巨额财富的算法。没有这一点,我们只是重复过去的错误,让深刻的经济鸿沟变得更加深刻。这将是困难的;目前几乎没有法律先例来说明今天发生的事情。
还必须解决生成式人工智能导致的职位流失和失业。大多数职位预计会受到人工智能的影响,且随着自动化的提高,失业率通常也会增加。根据BanklessTimes.com 的一份报告,到 2030 年,人工智能可能会取代 8 亿个工作岗位(占全球劳动力的 30%)。
对于一些人来说,这可能是在宏观经济层面上的问题(“AI 也可能通过自动化某些工作方面来改变职位角色并创造新的职位,同时允许人类专注于更具创意或增值的任务”,他们会这样说),但对其他人来说,却是数十年的绝望。我们需要一个针对被 AI 替代和自动化的人的监管计划(培训,UBI 等)。
最终,持续保护全球经济免受 AI 驱动的经济垄断将是非常重要的。网络效应意味着现在几乎不可能赶上互联网巨头,因为缺乏数据或计算能力。反垄断法已经被搁置了几十年,这种情况不能再继续下去。在这种情况下,监管不会让我们变得不具竞争力,反而可能使经济更加具备竞争力。
监管游戏才刚刚开始。展望未来,政府需要合作以建立广泛的框架,同时促进和鼓励知识共享和跨学科合作。
这些框架需要具备适应性和协作性,否则将无法跟上 AI 的最新发展。定期的审查和更新将是关键,敏捷实验也同样重要。
最终,公众参与和包容性的决策制定将决定任何出台的规则的成败。我们需要在监管讨论中涉及各种利益相关者,同时让公众参与 AI 政策决策。这是为了我们/他们,清晰传达这一点将帮助政府对抗科技公司的游说。
监管的道路漫长:目前,没有任何基础 LLM 符合 EU AI 法规。与此同时,中国的监管集中在内容控制 而非风险,进一步加强了党对言论自由的控制。
监管游戏才刚刚开始。不过……我们已经开始了,这才是关键所在。
还有一件事
请通过留言支持我的写作。我喜欢阅读留言!除了那些恶意的,它们让我伤心。另外,通过订阅我的通讯来避免错过我的任何写作 在 Medium 上,或者 在我的博客上。
这篇文章 最初为 wearedevelopers.com编写的,欧洲开发者专注的招聘平台。
反对企业 LLMs 的案例
观点
一种清醒的视角,说明为什么无聊可能是最好的,甚至对于 AI 来说也是如此
·
关注 发表在 数据科学之路 ·6 分钟阅读·2023 年 4 月 29 日
--
在过去的几周里,我们收到了大量来自客户和合作伙伴的定制 LLM 请求。这种兴奋感,虽然是合理的,但更多是基于技术新闻的轰炸,而不是获取根本的企业优势。
尽管 LLMs 在概念上并不比大多数基于变换器的训练管道差,但在企业环境中微调和顺利操作需要更复杂的机制。我们已经测试和部署给客户的所有模型都很好,但它们没有像抛光的商业产品那样的光泽,这对业务负责人来说是个问题。
别误解我的意思:LLMs 是自 GPU 加速训练以来 AI 中最棒的东西,但它们应该是最后的手段,而不是在企业 AI 领域中首次尝试的工具。
图片来自 Pixabay 见于 Pexels.com
“软件变慢的速度比硬件变快的速度快”是计算机科学中的一个古老谚语。软件容易膨胀,因为通常添加模块比删除模块要容易。
在 AI 方面,从机器学习模型到深度神经网络,再到 LSTM、预训练 CNNs 以及现在的 Transformers,模型的增长遵循了类似的路径。虽然使用顶级技术是合理的,但在某些情况下,如果总拥有成本超过任何已识别的收益,渐近收益就会变成递减回报。
在与客户的对话中,一个诡异的熟悉模式正在重新出现:那些今天推动 ChatGPT 克隆的高管,几年前对聊天机器人也是态度坚决的。这是一个容易取得可证明胜利的手段,而无需将其与企业价值关联或考虑其正当性。他们对于“你只是不看到价值”的宗教式坚持,充其量只是懒惰的陈词滥调,最坏的情况则是一个资源浪费变成企业负担。
在这些部署中出现的许多问题并不在于项目的理由或可行性,而是在于为什么从人类开发的最复杂技术中选择作为改善下一个季度业绩的有效首选的正当性。
AI 不是魔法粉;它在与数据、上下文和推断有效性的关系中创造价值。希望和祈祷通常不是应对技术债务的有效策略。美国足球比赛的胜利来自于战略和执行,而不是“绝望传球”。
我的偏见
作为一名工程师,我关注的是部署的可行性、总拥有成本以及性价比。我们的客户信任我们成为新技术及其在客户组织中的应用的透明裁决者。部署风险需要与商业回报和投资回报率进行评估。仅仅因为一项技术流行就实施它,通常会引发许多警报,这也是有道理的。
我们对 LLMs 并没有(无论是伦理的还是经济的)问题——我们热爱技术。我们有给 预训练模型做罗夏测试 的记录,构建了一个 星座交易机器人,甚至创建了一个带有迪斯科球的紧急聚会按钮。技术很酷。
然而,我必须坚持,如果你不知道你的问题是什么,技术将无法解决你的问题。更糟糕的是,与 LLMs 相关的工程挑战意味着每个典型项目风险现在都是交付的生存危机。
成功的样子
在我们经营AI 咨询公司的这些年里,AI 采纳成功的最大驱动因素是明确的成功衡量标准。这意味着:
-
业务背景已经通过 KPI 明确;
-
项目的需求已经确定;
-
项目的交付已经设定了目标。
质量管理系统,作为比较您的机器学习项目的自然尺度,需要在已建立的需求与验证/确认之间保持可追溯性,而新的管理技术则要求进行目标/关键结果(OKR)任务分配。围绕 AI 部署的期望也应根据可测量和客观的成功指标进行评估。
未提供理由的成本
尤其是对于 LLM,近年来 AI 推动的项目经历了自愿项目和必要项目之间的推拉力量。自愿项目有趣,并在传说中的饮水机旁成为有趣的故事;必要项目则是完成任务的单色西装。你在市场不确定性时更愿意拥有哪一种?
优秀的 AI,就像优秀的设计一样,应当是隐形的,而非焦点。
在 LLM 之前的众多选择
在考虑生成型 AI 之前,较老的基于变压器的模型和流程能够在不花费大量资金的情况下获得相同的商业结果。
大多数用例是知识库、历史分析和洞察生成,因此让我们看看可以找到哪些替代方法。
导航你的数据
在过去几年里,有两项技术使智能文本搜索变得轻而易举:句子嵌入和向量数据库。
自从最后几种词嵌入或子词嵌入技术以来,句子(或文档)嵌入确实成为了一个区分因素。对词序的意识(得益于位置编码)在细微差别中创造了更多的理解,并具有令人难以置信的复杂性导航。复杂的句子结构,甚至文档,可以可靠地向量化、聚类和比较。
向量数据库,许多是舒适的开源(例如Vald和Weaviate),已经包含了开箱即用的自我优化和近似最近邻搜索。
在这个简单模型的商业背景下,应用数量令人眼花缭乱:你现在拥有一个迷你搜索引擎,可以检索与您最新提案最相似的历史 RFP 句子,甚至可以查找和组织合同所需的相关文件。
这种方法的优势在于,你能够避免 LLM 的幻觉:排名结果首先提供了上下文价值,意味着你不需要深入研究前几个答案之外。要么你面前直接有一个答案,要么就没有。尽管这不如提示性反应的舒缓步调令人放心,信息却极为精准,即使没有有价值的结果,也可以体现出团队内部状态的迹象。
注:你甚至不需要 VD 来从相似搜索中获取价值。在多核系统上,带有余弦相似度的平面文件实际上足够快速,在企业环境中可用。如果你想亲自尝试,请将所有文档转换为 Markdown,并在标题之间分割文本。恭喜,你现在有了你的小型搜索引擎。
在 LLMs 之前(这有点名不副实;它似乎包含了生成文本的一切和所有),NLP 世界充斥着各种应用和明确定义的解决方案模式。(查看Papers With Code的 NLP 部分作为例子。)
构建一个句子或文档分类器仍然是组织数据的一种可靠方法;其中不乏强制组织认识其半空数据库的数据清理过程。
基础数据卫生到底发生了什么?
我无法再强调了:数据将解决你的 AI 问题;AI 不会解决你的数据问题。
我曾详细写过关于数据与 AI 的关系以及价值创造,AI 不仅生成见解,还有助于清理布满蜘蛛网的数字档案。在商业环境中,由于流程和历史文化,产生惯性是很自然的。这种战争迷雾源于个人之间关系的巨大数量,以实现愿景和使命的成功。
然而,在这些过程中,预计会发现不完整的表单和缺失的报告。精力不应该用来跳过这些问题,而应该用来正确地归档它们。
然而,LLMs 是未来。
关键的要点是,对于刚开始采用 AI 的公司而言,大多数情况下,先稳扎稳打比冒进更为明智。成功的项目交付能够节省资金,是一种全面稳固的策略。
LLMs 有着超凡的能力,可以处理复杂的思想,并在几秒钟内干净地总结它们,但大多数新闻文章只引用最佳案例,而不是达到这一点所需的总体努力量。就像社交媒体一样,现实往往是欺骗性的。如果看起来毫不费力,那可能并非如此。
我并不是在反对企业级 LLM;我在反对将企业级 LLM 作为首个 AI 项目。
您可能会喜欢的其他文章
-
PyTorch 与 TensorFlow 在基于 Transformer 的 NLP 应用中的比较
-
用于批处理的 MLOps:在 GPU 上运行 Airflow
-
数据集偏差:制度化歧视还是充分透明?
如果您对本文或我们的 AI 咨询有额外的问题,请随时通过 LinkedIn或 电子邮件联系我们。
-Matt.
《… Python 中的捕手:用一个工具捕获异常和警告》
PYTHON PROGRAMMING
为什么不在 Python 中实现一个同时捕获异常和警告的工具呢?
·发表于 Towards Data Science ·13 min read·2023 年 5 月 23 日
--
这篇文章讨论了在 Python 中捕获异常和警告的问题。照片由 Keith Johnston 拍摄,来源于 Unsplash
Python 有一些很好的异常处理方法。你可以捕获它们、引发它们、重新引发它们、同时引发两个异常(或者说,一个异常引发另一个异常)、记录它们,还可以创建自定义错误及其层次结构,只列举了最重要的一些。如果你想处理警告,你也可以做到——但你需要两个不同的工具:一个用于异常,一个用于警告。
阅读这篇文章,了解如何编写一个同时捕获异常和警告的工具,以及如何根据需求调整它。
我非常喜欢 Python 的异常处理。我喜欢它的简洁性,比如这里展示的:
class MultiplicationError(Exception): ...
def multiply_str(x: str, n: int) -> str:
try:
xn = x * n
except TypeError as e:
raise MultiplicationError(
"Can't multiply objects of "
f"{type(x).__name__} "
f"and {type(n).__name__} types"
) from e
if not isinstance(xn, str):
raise MultiplicationError(
f"{type(x).__name__} multiplied by"
f" {type(n).__name__} "
"does not give str object by "
f"{type(xn).__name__}"
) from TypeError
return xn
在分析函数之前,让我们看看它的实际应用:
multiply_str() 函数的实际应用;来自 Python REPL 的截图。图片由作者提供
前两个调用是正确的。第二个调用不正确,因为我们试图乘以两个字符串。第三个调用也不正确,但这次是因为函数接收了两个整数值。它们可以被相乘,但我们的函数通过检测结果对象不是字符串来捕获这个场景——这基本上意味着出了问题,因此产生了异常。
我们在这里看到的被称为鸭子类型。我们不是检查输入值是否具有正确的类型;而是执行函数的目标——在这里是乘以两个值。如果一切顺利,那么……就是好的。鸭子类型是 Python 中一种典型的动态方法。
然而,在这个函数中,情况要复杂一些:该函数也将与两个数值一起工作,这是我们不感兴趣的。我们不希望函数具有多态性:我们希望它与 x
为字符串和 n
为整数一起工作。
因此,我们可以检查它们的类型,并引发与上述相同的异常。在上述版本中,我们检查返回值的类型,这是一种更快的方法。它更快,因为我们只检查一个对象的类型(xn
),但仅在我们知道 x
和 n
可以相乘时才进行检查。因此,这并不是真正的鸭子类型。然而,通常你会选择纯粹的鸭子类型,这是最自然的 Python 方法。
然而,在这篇文章中,我们讨论的是别的内容:捕获错误,所以让我们继续。multiply_str()
函数将一个字符串 x
乘以一个整数 n
,但以受控的方式进行——这意味着如果出现问题,相应的异常会被捕获并使用自定义异常引发。实际上,它做的还不止这些:
-
已添加自定义消息。
-
MultiplicationError
从TypeError
中引发。
后者在我们讨论的背景下很有趣。实际上,它帮助你使用 except MultiplicationError
和 except TypeError
捕获 multiply_str
的错误。两者都会捕获 MultiplicationError
—— 即使 TypeError
与前者没有(程序上)关系。所以,这:
try:
return multiply_str("test", "it")
except TypeError as e:
print(e)
return multiply_str("test", 1)
将捕获与此相同的错误:
try:
return multiply_str("test", "it")
except MultiplicationError as e:
print(e)
return multiply_str("test", 1)
这在各种情况下都很有用,尤其是当一个人在其自己定义 multiply_str()
的包之外使用 multiply_str()
时。否则,用户将不得不导入 MultiplicationError
。尝试捕获 TypeError
更简单,这也允许你捕获 MultiplicationError
。
当然,Python 中使用异常的内容远不止这些。你可以从这篇文章中了解更多关于自定义异常的内容:
Python 有这么多内置异常,以至于我们很少需要创建和使用自定义异常。或者说,我们需要吗?
towardsdatascience.com
你可能还会发现这篇文章很有趣:
[## 如何在 Python 中重写 AssertionError 并使用自定义异常
Python 的 assert 语句使用 AssertionError。了解如何使用其他异常代替。
betterprogramming.pub](https://betterprogramming.pub/how-to-overwrite-asserterror-in-python-and-use-custom-exceptions-c0b252989977?source=post_page-----81714117da76--------------------------------)
但现在,让我们继续讨论警告。
处理警告
警告的引发和捕获方式类似。
import warnings
class MultiplicationError(Exception): ...
class MultiplicationWarning(Warning): ...
def multiply_str(x: str, n: int) -> str:
try:
xn = x * n
except TypeError as e:
raise MultiplicationError(
"Can't multiply objects of "
f"{type(x).__name__} "
f"and {type(n).__name__} types"
) from e
if not isinstance(xn, str):
warnings.warn(
f"{type(x).__name__} multiplied by"
f" {type(n).__name__} "
"does not give str object by "
f"{type(xn).__name__}",
MultiplicationWarning
)
return xn
当乘法过程中抛出异常时,我们仍然会抛出异常,但当返回的对象不是字符串时,会发出警告。看看这是如何工作的:
multiply_str()
函数的实际操作,这次有异常和警告;来自 Python REPL 的截图。图片由作者提供
如你所见,得到警告时,你会收到警告,但也会得到输出,这与异常的情况不同。你需要记住,Python 的默认行为是只警告一次:
默认行为是只发出一次特定的警告;来自 Python REPL 的截图。图片由作者提供
这假设你会记住你已经被警告过,所以你得自己解决。你可以改变这种行为,但我们会在其他时间讨论这个问题。改变这种行为很简单。请看下面的截图来自一个新会话,代码与之前导入的一样:
一个“始终”警告过滤器会改变默认行为;来自 Python REPL 的截图。图片由作者提供
从现在开始,我们将使用这个警告过滤器。
警告远不止这些。让我们在这里停下,今天我们主要的目标是实现一个可以同时捕捉异常和警告的工具。
定义捕捉器
好的,我们要实现一个工具,使我们能够捕捉到异常和警告。首先,让我们看看如何捕捉我们自定义的MultiplicationError
:
my_string = 10
factor = 2
try:
mult_string = multiply_str(my_string, factor)
except MultiplicationError as e:
mult_string = my_string
print(
"Error during multiplication of "
f"{my_string} by {factor}: {e}"
)
和警告:
with warnings.catch_warnings(record=True) as w:
mult_string = multiply_str(my_string, factor)
当然,这些只是一些基本示例。我们可以根据希望返回给用户的内容以各种方式实现它。暂时,我们返回一个按捕捉时间排序的异常和警告列表。
这是最简单的例子之一:
from typing import Callable
def catch(func: Callable) -> list[str]:
"""Catch issues and warnings using one function."""
to_return = None
issues = []
with warnings.catch_warnings(record=True) as warning:
try:
to_return = func()
except Exception as e:
issues.append(f"Exception: {e}")
finally:
for w in warning:
issues.append(f"Warning: {w.category.__name__}:"
f" {w.message}")
return to_return, issues
让我们看看它是如何工作的:
>>> class DivisionWarning(Warning): ...
>>> def divdiv(x, y):
... warnings.warn("Watch out!", DivisionWarning)
... return x / y / x
>>> results = catch(lambda: divdiv(1, 0))[1]
>>> results
(None, ['Exception: division by zero', 'Warning: DivisionWarning: Watch out!'])
divdiv()
函数确实不自然,因为它总是发出警告,但我想展示一个非常简单的例子。运行不正确的命令 divdiv(1, 0)
后,捕捉器包含了一个异常和一个警告。
我们可以稍微改进一下catch()
函数。虽然许多 Python 爱好者喜欢从函数返回两元素或三元素的元组,但你也可以返回一个命名元组:
from collections import namedtuple
def catch(func: Callable) -> list[str]:
"""Catch issues and warnings using one function."""
to_return = None
issues = []
with warnings.catch_warnings(record=True) as warning:
try:
to_return = func()
except Exception as e:
issues.append(f"Exception: {e}")
finally:
for w in warning:
issues.append(f"Warning: {w.category.__name__}:"
f" {w.message}")
CatchReturn = namedtuple("CatchReturn", "to_return issues")
return CatchReturn(to_return, issues)
我们可以在函数外定义CatchReturn
;也可以在函数内部动态定义,使用以下返回行替代上面函数的最后两行:
return namedtuple("CatchReturn", "to_return issues")(to_return, issues)
无论如何,使用这个命名元组,函数肯定会返回一个命名元组:
>>> class DivisionWarning(Warning): ...
>>> def divdiv(x, y):
... warnings.warn("Watch out!", DivisionWarning)
... return x / y / x
>>> results = catch(lambda: divdiv(1, 0))
>>> results
CatchReturn(to_return=None, issues=['Exception: division by zero', 'Warning: DivisionWarning: Watch out!'])
>>> results.to_return # None
好的一点是,你可以忽略它是一个命名元组,将它像普通元组一样对待:
>>> to_return, issues = catch(lambda: divdiv(1, 0))
>>> to_return # None
>>> issues
['Exception: division by zero', 'Warning: DivisionWarning: Watch out!']
好的,这就是一个基本版本的联合捕获器。但你可以用这样的捕获器做更多的事情!下面,我展示了两个更多的示例,只有基本的注释。你可以享受这些示例的时间以及理解它们实现的努力。如果你有任何问题,请随时在评论中提问。如果你想到其他你想分享的示例,请务必告诉我!
示例:基于闭包的多个捕获器
之前的捕获器与单个函数调用一起工作。在这个示例中,我们将使用闭包来构建一个可以用于许多后续函数调用的捕获器。如果某个调用顺利进行,捕获器将简单地返回结果。如果发出了警告,它将返回输出并记录警告。如果引发了异常,它将捕获异常——但完全不会返回。
注意实现:我使用了闭包。Python 中闭包最常见的例子是装饰器,但闭包也可以用于其他目的。在这里,我们将使用它们来保持状态;我们可以使用类(实际上,我们将在下一个示例中这样做),但正如你将看到的,闭包提供了一种更简单的方法。
def catch_all() -> list[str]:
"""Catch multiple issues and warnings using one function."""
issues = []
def catch_one(func: Optional[Callable] = None) -> Callable:
to_return = None
if not func:
return issues
with warnings.catch_warnings(record=True) as warning:
try:
to_return = func()
except Exception as e:
issues.append(f"{type(e).__name__}: {e}")
finally:
for w in warning:
issues.append(f"{w.category.__name__}:"
f" {w.message}")
return to_return
return catch_one
现在,让我们看看这个函数的实际操作。首先,我们需要创建一个捕获器对象:
>>> all_catcher = catch_all()
我们现在将使用all_catcher
对象来调用多个函数。我将使用与上面相同的DivisionWarning
和divdiv()
函数。
>>> x1 = all_catcher(lambda: divdiv(1, 0))
>>> x1 # this is None
这个捕获器与定义为lambda
的函数一起使用,尽管我们可以使用不同的结构;我们将在下一个示例中这样做。上面,x1
是None
,因为除了DivisionWarning
之外,捕获器还捕获了一个异常(ZeroDivisionError
)。要查看issues
列表,只需调用all_catcher()
(感谢catch_one()
函数中的if
块):
>>> all_catcher()
['ZeroDivisionError: division by zero', 'DivisionWarning: Watch out!']
如果我们调用一个正常工作的函数,我们将得到其返回值,捕获器将不会记录任何内容(所以其issues
列表将保持不变):
>>> x2 = all_catcher(lambda: divdiv(1, 1))
>>> x1
1.0
>>> all_catcher()
['ZeroDivisionError: division by zero', 'DivisionWarning: Watch out!']
让我们再添加一些调用:
>>> all_catcher(lambda: divdiv(1, 0)) # None again
>>> all_catcher(lambda: 1 + 1)
2
捕获器的网应该包含比之前更多的内容¹:
>>> all_catcher() # doctest: NORMALIZE_WHITESPACE
['ZeroDivisionError: division by zero',
'DivisionWarning: Watch out!',
'ZeroDivisionError: division by zero',
'ZeroDivisionError: division by zero',
'DivisionWarning: Watch out!']
它确实如此。错误和警告在这里按捕获顺序记录,但这只是各种可能性中的一种。
哦,还有一件事。你可以在一个会话/脚本中使用更多这样的捕获器:
>>> catcher1 = catch_all()
>>> catcher2 = catch_all()
>>> catcher1(lambda: 1 / 0)
>>> catcher2(lambda: 1 * 2)
2
与catcher1
不同,catcher2
应该是空的。让我们检查一下:
>>> catcher1()
['ZeroDivisionError: division by zero']
>>> catcher2()
[]
作为另一个练习,你可以在catch_all()
中使用不同的数据结构,而不是列表。或者将异常和警告记录到不同的列表中。或者使用logging
模块记录。或者为每个元素添加时间戳。
示例:基于类的捕获器
我们可以实现一个基于类的捕获器,它的工作方式与我们上面使用闭包实现的捕获器相同,但我将把这留给你作为练习。相反,我将向你展示如何做一些更有趣的事情,即实现一个与|
管道运算符一起工作的捕获器。
在 Python 中,管道运算符不太流行,但它们确实存在。你可以在Pipe
包中看到它们:
## GitHub - JulienPalard/Pipe: 用于在 Python 中使用中缀表示法的 Python 库
模块启用类似 sh 的中缀语法(使用管道)。作为示例,这里是第 2 个欧拉项目的解决方案……
虽然 Python 不是一种函数式编程语言,但它提供了各种工具,你可以用来至少模拟函数式编程范式。Pipe
包是一个很好的示例,展示了如何使用管道来实现这一点。
然而,我们将实现我们自己的管道操作符,通过重载|
操作符:
class Catcher:
def __init__(self):
self.issues = []
def __or__(self, other):
to_return = None
with warnings.catch_warnings(record=True) as w:
try:
to_return = other()
except Exception as e:
self.issues.append(f"Exception: {e}")
finally:
for ww in w:
self.issues.append(f"Warning: {ww.category.__name__}:"
f" {ww.message}")
return to_return
def __call__(self):
return self.issues
def __repr__(self):
n = len(self.issues)
msg = "A Catcher instance. Until now, it caught"
if not n:
msg = f"{msg} no exceptions and warnings."
return msg
msg = f"{msg} {n} element{'s' if n > 1 else ''}."
return msg
catcher = Catcher()
让我们使用这个catcher
:
>>> catcher
A Catcher instance. Until now, it caught no exceptions and warnings.
>>> catcher | (lambda: divdiv(1, 1))
1.0
>>> catcher
A Catcher instance. Until now, it caught 1 element.
>>> catcher()
['Warning: DivisionWarning: Watch out!']
>>> to_return = catcher | (lambda: divdiv(1, 1))
>>> to_return
1.0
>>> catcher
A Catcher instance. Until now, it caught 2 elements.
这是基本用法。让我们定义几个助手,以便给我们一些灵活性:
import functools
class Printer:
def __ror__(self, other, **kwargs):
print(other, **kwargs)
class Ignorer:
def __ror__(self, other):
pass
Run = functools.partial
Print = Printer()
Ignore = Ignorer()
Run
函数可以用来替代lambda
语法(下面,你可以比较这两种调用函数的方式):
>>> to_return = catcher | Run(divdiv, 1, 1)
>>> to_return
1.0
>>> to_return = catcher | (lambda: divdiv(1, 0))
>>> to_return # None
>>> catcher() # doctest: NORMALIZE_WHITESPACE
['Warning: DivisionWarning: Watch out!',
'Warning: DivisionWarning: Watch out!',
'Warning: DivisionWarning: Watch out!']
但是如果你使用Print
来显然地打印,你会看到None
:
>>> catcher | Run(divdiv, 0, 0) | Print
None
>>> catcher
A Catcher instance. Until now, it caught 7 elements.
>>> catcher() # doctest: NORMALIZE_WHITESPACE
['Warning: DivisionWarning: Watch out!',
'Warning: DivisionWarning: Watch out!',
'Warning: DivisionWarning: Watch out!',
'Exception: division by zero',
'Warning: DivisionWarning: Watch out!',
'Exception: division by zero',
'Warning: DivisionWarning: Watch out!']
但是使用Print
时,捕获器仅仅打印而不返回任何东西。你可以修改它,使其同时完成这两项功能,在这种情况下,它可以作为一个简单的调试工具。
我们剩下的是Ignore
。它并不会真正忽略与之运行的函数;它忽略该函数的任何输出,捕获异常和警告:
>>> catcher | Run(divdiv, 0, 0) | Ignore
>>> catcher
A Catcher instance. Until now, it caught 9 elements.
Run
、Print
和Ignore
只是三个示例,如果你有空闲时间,可以考虑其他可以与这个捕获器一起使用的示例。请注意,我使用了一种不典型的——甚至非 Pythonic 的——方法,其中我用大写字母命名与函数类似的类实例(好吧,更准确地说是类似于函数),以区分Print
和内置的print
。虽然其他对象在标准 Python 库中没有对应物,我还是希望对所有这些与捕获器一起工作的对象使用相同的命名约定。
我并不是说这是一种好的方法——但我认为展示一下在某些情况下你可以尝试使用不典型语法是个好主意。然而,如果你决定这样做,即走非主流路线,请始终三思你所做的是否对 Python 来说过于不典型。并准备好捍卫你的立场。
结论
这篇文章旨在展示为什么实现一个联合捕获警告和异常的工具是值得的,并演示如何实现一些示例。我的目标不是为你实现一个可以在项目中使用的工具,而是展示如何实现这样的工具。
这并不意味着我们讨论的捕获器还没有准备好直接使用;它们已经准备好了。不过,我希望现在你已经掌握了这些知识后,能够实现适应你项目的自定义捕获器。
如果你有任何评论或建议——无论是对我还是对其他读者——请随时在评论中分享。
脚注
¹ 你可以在代码中看到一个doctest
标志,因为我使用它来测试本文中的代码。doctest
是一个用于文档测试的标准库模块。如果你有兴趣了解更多关于这个有趣模块的内容,可以参考这篇文章:
## 使用 doctest 进行 Python 文档测试:简单方法
doctest 允许进行文档、单元和集成测试,以及测试驱动开发。
towardsdatascience.com
感谢阅读。如果你喜欢这篇文章,你也可能会喜欢我写的其他文章;你可以在这里查看。如果你想加入 Medium,请使用下面的推荐链接:
[## 使用我的推荐链接加入 Medium - Marcin Kozak
阅读 Marcin Kozak 和其他成千上万的 Medium 作者的每一篇故事。你的会员费直接支持…
medium.com](https://medium.com/@nyggus/membership?source=post_page-----81714117da76--------------------------------)
看到 AI 大局的挑战
·
关注 发表在 Towards Data Science · 以 Newsletter 形式发送 · 3 分钟阅读 · 2023 年 6 月 15 日
--
关于 AI 在我们日常生活中不断变化(且迅速扩张)的影响,进行深思熟虑的对话变得越来越困难——这也许是技术从研究和工业界进入到社交媒体的各个角落所带来的意外副作用。确实有很多炒作,还有不少的末日预言以及源源不断的新工具和应用。但这一切到底意味着什么?
在过去几个月里,我们与读者分享了一些出色的实践资源,帮助你熟悉诸如提示工程和语音转文本自动化等实际话题。今天,我们邀请你后退一步(或两步),探讨我们作者在应对 AI 在数据科学和机器学习工作流程中不断变化的角色时撰写的一些更宏大的主题。让我们深入了解吧。
-
近年来许多最令人兴奋的创新成功归功于基础开源项目。克莱门斯·梅瓦尔德认为那个时代即将结束:“尽管似乎有一种类似罗宾汉的开源 AI 运动,但数据指向的方向却有所不同。”
-
即使是最热衷的 ChatGPT 粉丝也不得不承认,这款聊天机器人存在严重的局限性,从它的幻觉倾向到无法提供实时(甚至只是稍微更新的)信息。玛丽·纽豪泽最近调查了 ChatGPT 插件的广阔领域—这些应用程序为工具添加新功能并将其连接到其他数据源,并报告了它们的好处和风险。
-
对于许多机器学习从业者而言,ChatGPT 的到来标志着一个分水岭时刻,促使他们对项目、实践和商业模式进行严肃的重新思考。贾娜·利彭科娃在这篇文章中概述了这一后 ChatGPT 生态系统中的四大新兴趋势,并反思了它们将如何影响未来的 AI 发展。
-
“AI 会使其过时吗?”列表上的职业每天都在增加。加伦·冈崎将这一问题带入现实(至少对《Variable》的读者而言),并探讨了数据分析师的未来会是什么样,随着生成型 AI 工具的不断成熟。(剧透:人类仍然有一线生机!)
-
如果 AI 真正要改变世界的运作方式,一个将促进这种转变的领域是供应链管理。Ezequiel Ortiz Recalde解读了这个巨大且全球范围的领域中的一些可能性,从库存优化到多式联运物流。
正如我们喜欢提醒自己(以及任何愿意倾听的人),在其他非 AI 主题上也有许多引人入胜的工作;如果您需要进一步证明,以下推荐的阅读材料都可以:
-
Christabelle Pabalan向我们介绍了她关于使用多任务和集成学习预测阿尔茨海默病认知功能的重要工作。
-
如果图神经网络中的输入图是有向的会发生什么?Michael Bronstein和 Emanuele Rossi 最新的文章详细探讨了这个问题。
-
Subha Ganapathi的有用新教程介绍了在 API 端点不可用时,数据库触发器作为访问数据的替代方法。
-
如何确保您的数据团队是主动而非被动的?Barr Moses反思了那些能够带来改变的实践和特征。
-
为您的深度学习项目选择正确的硬件至关重要;Chaim Rand的指南提供了关于您应该询问的问题的明确指引以及您需要考虑的因素。
感谢您支持我们的作者!如果您喜欢在 TDS 上阅读的文章,可以考虑成为 Medium 会员——这将解锁我们整个档案(以及 Medium 上的所有其他帖子)。
直到下一个 Variable,
TDS 编辑们
《混沌数据工程宣言》
我们可以从软件工程师那里学到的另一个教训是:破坏现有系统,以便让它变得更加可靠。
·
关注 发表在 Towards Data Science ·12 min read·2023 年 2 月 24 日
--
照片由 Soheb Zaidi 提供,刊登于 Unsplash
在《纽约时报》作为“指挥室”使用的昏暗凌乱的办公室里已是午夜时分。
强大的流量冲击是不可避免的。在每次重大选举期间,流量会达到高峰,然后猛烈冲击我们不堪重负的系统,随后才会退去,让我们能够评估损害。
我们已经在云环境中待了多年,这有帮助。我们的主要系统可以扩展——我们的文章总是能被服务——但后端服务中的集成点最终会在疯狂的流量压力下崩溃。
然而,2020 年的这个夜晚与 2014 年、2016 年和 2018 年的类似选举之夜有所不同。因为这次流量激增是模拟的,而不是实际发生的选举。
推至失败的边缘
模拟与否,这确实是生产环境,所以风险很高。当我们的系统 J-Kidd——它将广告定位参数带到前端——崩溃时,压抑的恐惧感蔓延开来。这就像是所有的韧带都从以其名字命名的传球控卫的膝盖上被撕扯下来一样。哎呀。
通过 维基共享资源。 版权归 KeithAllisonPhoto.com 所有。 本文件采用创意共享 署名-相同方式共享 2.0 通用 许可协议授权。
J-Kidd 并不是唯一一个被列入禁用名单的系统。这次演练的目的就是将我们的系统推向极限,直到它们失败。我们成功了。或者说,从你的角度来看,我们失败了。
第二天,团队进行了调整。我们拆分了系统,实施了安全机制,然后回到赛场迎接第二场比赛。因此,2020 年的选举是我记得的第一次,值班工程师们没有绷紧神经,紧握键盘……至少不是因为系统可靠性的问题。
预先检讨和混沌工程
我们把这次演练称为“预先检讨”。其概念根源可以追溯到由站点可靠性工程师提出的混沌工程。
对于不熟悉的人来说,混沌工程是一种有纪律的方法论,旨在有意地在系统内引入故障点,以更好地理解其阈值并提高弹性。
这一方法在很大程度上是通过Netflix 的猩猩军团的成功而广泛流行起来的,该程序套件会通过删除服务器、区域以及引入其他故障点来自动引入混沌。所有这些都是为了可靠性和弹性。
尽管这一理念在数据工程中并非完全 陌生 的,但它确实可以被描述为一种极为罕见的实践。
任何理智的数据工程师都不会看着自己的待办事项清单、团队中未填补的职位、管道的复杂性,然后说:“这需要更难一点。我们引入一些混乱吧。” 这可能是问题的一部分。
数据团队需要超越提供数据质量快照的传统思维,开始考虑如何构建和维护可扩展的可靠数据系统。
我们不能忽视数据在关键操作中的日益重要角色。仅今年,我们就目睹了一次文件删除和一个不同步的遗留数据库如何使超过 4000 个航班停飞。
当然,你不能直接将软件工程概念复制粘贴到数据工程手册中。数据是不同的。DataOps 对 DevOps 方法论的调整就如同数据可观察性对可观察性的调整。
那么,请把这个宣言视为如何将混乱工程的验证概念应用于数据这个特殊领域的提案。
数据混乱工程的 5 条法则
混乱工程的原则和经验教训是定义数据混乱工程学科轮廓的良好起点。我们的第一条法则结合了两个最重要的方面。
第一条法则:对生产保持偏见,但要最小化爆炸半径
在站点可靠性工程师中有一句格言,对于每一位曾经历过相同 SQL 查询在测试和生产环境中返回不同结果的数据工程师来说,这句话一定会引起共鸣:“没有什么像生产环境那样运作,除了生产环境。”
我会补充一句:“生产数据也是如此。” 数据过于创造性和流动,难以被人类预见。合成数据已经取得了很大进展,别误解我的意思,它可以是拼图中的一部分,但不太可能模拟关键的边缘情况。
像我一样,单单想到将故障点引入生产系统可能会让你感到不安。这是令人恐惧的。一些数据工程师正当地会怀疑:“在一个如此多工具抽象了基础设施的现代数据栈中,这真的有必要吗?”
我也担心如此。正如开头的轶事和 J-Kidd 的韧带断裂所示,云的弹性并不是万能的。
实际上,正是这种抽象性和不透明性——加上多个集成点——使得对现代数据堆栈进行压力测试变得如此重要。虽然本地数据库可能限制更多,但数据团队倾向于在日常操作中更频繁地遇到其阈值,因此对其有更好的理解。
让我们暂时跳过哲学上的异议,深入实际。数据是不同的。将假数据引入系统不会有帮助,因为输入改变了输出。这也会变得非常混乱。
这就是法律的第二部分发挥作用的地方:最小化爆炸半径。有一个混乱的光谱和可以使用的工具:
-
仅用文字来说,“假设这失败了,我们该怎么办?”
-
生产中的合成数据
-
像数据差异化这样的技术允许你在生产数据上测试 SQL 代码片段
-
像 LakeFS 这样的解决方案允许你通过创建“混乱分支”或生产环境的完整快照,在更大规模上实现这一点,你可以使用生产数据,但完全隔离。
-
在生产环境中进行操作,并练习你的数据回填技能。毕竟,没有什么能像生产环境那样模拟生产环境。
从较少混乱的场景开始可能是个好主意,并将帮助你理解如何在生产环境中最小化爆炸半径。
深入研究真实生产事件也是一个很好的起点。每个人真的理解发生了什么吗?生产事件是你已经支付的混乱实验,因此确保你从中获得最多的价值。
减少爆炸半径还可能包括诸如备份相关系统或拥有数据可观察性或数据质量监控解决方案的策略,以协助检测和解决数据事件。
第二法则:理解没有一个完美的时机(在合理范围内)
另一个混乱工程原则是观察和理解“稳定状态行为”。
这一原则有其智慧,但同样重要的是要理解,数据工程领域还未完全准备好按照“5 9s”或 99.999% 的正常运行时间标准进行衡量。
数据系统始终处于变化中,并且有更广泛的“稳定状态行为”。会有诱惑推迟引入混乱,直到你达到了所谓的“准备”点。好吧,你无法通过架构应对糟糕的数据,也没有人真正准备好面对混乱。
硅谷的“快速失败”陈词在这里适用。或者引用 Reid Hoffman,如果你对第一次预演/火灾演习/引入混乱事件的结果不感到尴尬,那你引入它的时机太晚了。
在处理真实数据事件时引入假数据事件可能看起来很傻,但最终这可以帮助你更好地了解你在什么地方用创可贴解决了可能需要重构的更大问题。
第三法则:制定假设并识别系统、代码和数据层级的变量
混沌工程鼓励形成关于系统如何反应的假设,以了解需要监控的阈值。它也鼓励利用或模拟过去的真实世界事件或可能发生的事件。
我们将在下一节中深入探讨这些细节,但这里的重要修改是确保这些涵盖系统、代码和数据层级。每个层级的变量都可能产生数据事件,以下是一些快速示例:
-
系统:你的数据仓库中没有设置正确的权限。
-
代码:一个糟糕的左连接。
-
数据:第三方发送给你带有一堆 NULLS 的垃圾列。
模拟增加的流量水平和关闭服务器会影响数据系统,这些都是重要的测试,但也不要忽视数据系统可能出现的独特和有趣的故障方式。
第四法则:所有人都在一个房间里(或者至少是 Zoom 通话)
这个法则基于我的同事、站点可靠性工程师和混沌实践者Tim Tischler的经验。
“混沌工程不仅关乎系统,也关乎人。这两者一起演变,无法分开。这些练习的一半价值来自于把所有工程师放在一个房间里,问‘如果我们做 X 或者做 Y,会发生什么?’你一定会得到不同的答案。一旦你模拟了事件并看到了结果,现在每个人的思维地图都一致了。这是非常宝贵的,”他说。
此外,数据系统和职责的相互依赖性即使在最好的团队中也会造成所有权模糊。常常在这些重叠和责任空白的地方发生并被忽视,例如数据工程师、分析工程师和数据分析师互相指责的地方。
在许多组织中,创建数据的产品工程师和管理数据的数据工程师被团队结构分隔开。他们还常常拥有不同的工具和相同系统与数据的模型。尤其是当数据来自内部构建的系统时,欢迎把这些产品工程师也拉进来。
良好的事件管理和分类通常涉及多个团队,把每个人都放在一个房间里可以让练习更高效。
我也从个人经验中补充,这些练习可以很有趣(以同样奇怪的方式,就像把所有筹码押在红色上很有趣)。我建议数据团队在下一次离线会议中考虑进行一次混沌数据工程演习或预先检讨活动。相比于逃脱房间,这更是一种实际的团队凝聚力练习。
第五法则:暂时不要自动化
像 Netflix 的 Simian Army 这样的真正成熟的混沌工程程序是自动化的,甚至是非计划的。虽然这可能创造出更准确的模拟,但现实情况是,目前数据工程还没有自动化工具。如果有的话,我不确定自己是否足够勇敢去使用它们。
到目前为止,Netflix 的一位原始混沌工程师 描述过 他们并不总是使用自动化,因为混沌可能会比他们能解决的问题更多(特别是在与运行系统的人员合作时),在合理的时间内解决这些问题。
鉴于数据工程目前的可靠性演变以及潜在的意外大范围影响,我建议数据团队更倾向于安排有计划、仔细管理的事件。
数据混沌工程变量,也就是要破坏的内容
如果不提供更多具体的测试示例或从中获得的价值,建议数据工程师彻底改革他们的流程并引入更高的风险有些不够实际。
以下是一些建议。
系统级别
在这个级别,你应该测试你 IT 和数据系统之间的集成点。这包括从摄取和存储到编排和可视化的数据管道。你可以通过以下方式制造一些混乱:
-
在 Postgres 中制造问题- 你可能会发现你需要将操作系统与分析系统解耦。
-
终止 Airflow 作业或 Spark 集群- 你可能会发现你需要警报来通知团队这些失败发生时,并 自愈管道设计。
-
改变 Spark 核心执行器的数量- Zach Wilson 指出 增加更多的 Spark 执行器核心可能会负面影响可靠性而不是提升性能。对此进行实验。
-
调整 Snowflake 中的自动挂起策略- 很多团队正在努力优化他们的数据云成本,很容易削减过度而导致可靠性问题。例如,如果你将自动挂起设置得比查询负载中的任何间隙都要严格,数据仓库可能会陷入持续的自动挂起和自动恢复状态。这可能帮助你发现你需要按数据仓库大小重新组织和分类数据工作负载。或者,可能对谁有权限和权威进行这些类型的更改要更加审慎。
-
取消发布仪表盘- 我称之为尖叫测试。如果在取消发布仪表盘几周后没有人提及,可能可以考虑淘汰它。这也可以帮助测试你在数据工程和分析团队中的检测和沟通技能。
-
模拟流处理管道的连接失败- Mercari 数据团队迅速巧妙地解决了这个问题,得益于数据监控警报。他们暂时计划模拟类似问题,以训练他们的检测、处理和解决流程。
“我们的想法是通过角色扮演特定数据集的警报,并逐步演示事件处理、根本原因分析和与业务沟通的过程,”Mercari 数据可靠性工程团队的成员 Daniel 说道。“通过实际演练,我们可以更好地了解恢复计划,如何优先确保最重要的表格首先恢复,以及如何与跨越四个时区的团队沟通。”
代码层
当你移除那段代码块时,会发生什么?一切都会崩溃吗?照片由 Michał Parzuchowski 拍摄,发布于 Unsplash
这个层级是 ETL 或 ELT 过程中的 T(转换)。当你的 dbt 模型或 SQL 查询出现问题时会发生什么?是时候了解一下了!
-
存档一个 dbt 模型- 类似于仪表板的极限测试或积木游戏。这个模型真的需要吗?还是当你移除一个块时结构仍然稳定?这有助于评估你的测试和监控;是否存在未知的依赖关系;以及所有权的划分。
-
调整关键指标的计算方式- 当组织内存在多个“真实”版本时,相互冲突的趋势不可避免地会削弱对数据的信任。关键指标的变更是否会流动到所有下游报告中?是否存在不规范的定义?这有助于评估定义关键指标的组织流程,以及你是否需要投资于一个语义层。
-
让其他领域解读你的模型或 SQL 代码- 当然,某个工程师或团队可能理解特定领域中的模型,但当其他人需要理解时会发生什么?是否有良好的文档?其他人能否协助处理事件?
数据层
软件工程师在代码和系统层面也处理可靠性问题,而只有数据工程师必须在数据层面解决潜在问题。我提到过数据是不同的吗?
这个层级不能被忽视,因为你的 Airflow 任务可能运行正常,你的数据差异可能检查通过,但运行的数据可能完全是垃圾。垃圾进,垃圾出(也许系统崩溃)
-
更改架构—软件工程师经常无意中提交更改数据输出方式的代码,从而破坏下游数据管道。你可能需要解耦系统,并且对于某些操作管道,需要一个数据契约架构(或其他更好的工程协调流程)。
-
生成错误数据—这可能会暴露出你的模型对数据源表中的波动、空值等不够稳健。
-
复制数据—数据的剧增是否暴露出导致作业失败的瓶颈?你是否需要对某些管道进行微批处理?你的管道在需要时是否具备幂等性?
我们在谈论训练
我们以一个控球后卫开始了这个宣言,也许我们用这句臭名昭著的 2002 年名言来自由后卫艾伦·艾弗森作为对那些不相信“训练如同比赛”的警告,是合适的:
“我们在这里谈论的是训练。我是说,听着,我们在谈论训练。不是比赛!不是比赛!不是比赛!我们在谈论训练。不是比赛;不是我为之拼命并每场比赛都像是最后一场的比赛,不是比赛,我们在谈论的是训练,伙计。我的意思是,这有多荒谬?我们在谈论的是训练。
我知道我应该在这里,我知道我应该以身作则,我知道的。我并不是把它当作无关紧要的事情来推开。我知道这很重要。我知道的。我确实知道。但是我们在谈论的是训练,伙计。我们在谈论什么?训练?我们在谈论训练,伙计!
我们在谈论的是训练!我们在谈论的是训练……我们不是在谈论比赛!我们在谈论训练,伙计!”
那一年,76 人队在东部会议首轮季后赛中以五场比赛输给了波士顿凯尔特人队。艾弗森在系列赛中的投篮命中率为 38.1%。
关注我 在 Medium 上获取更多关于数据领导力、数据科学应用及相关主题的故事。 订阅 以便将我的故事发送到你的邮箱。
CLIP 基础模型
原文:
towardsdatascience.com/the-clip-foundation-model-7770858b487d
🚀Sascha 的论文俱乐部
从自然语言监督中学习可转移的视觉模型 作者:A. Radford 等
·发表于Towards Data Science ·阅读时间 8 分钟·2023 年 8 月 26 日
--
在这篇文章中,我们将深入探讨 CLIP(Contrastive Language-Image Pre-Training)背后的论文。我们将提取关键概念并将其分解,以便于理解。此外,图片和数据图表也会进行注释,以澄清疑问。
图片由Sascha Kirch基于出版物创建
论文: 从自然语言监督中学习可转移的视觉模型,Alec Radford 等,2021 年 2 月 26 日
资源: GitHub — 博客文章 — Hugging Face
类别: 多模态深度学习,计算机视觉,自然语言处理,基础模型,表征学习
[BYOL] — [Depth Anything] — [GLIP] — [Segment Anything] — [DINO] — [DDPM]
大纲
-
背景与背景
-
方法
-
实验
-
进一步阅读与资源
背景与背景
CLIP(对比 语言-图像 预训练)是一个多模态模型,学习自然语言与图像之间的对应关系。它在互联网上收集的 4 亿对文本-图像数据上进行训练。正如我们在本文后面将深入探讨的,CLIP 具有强大的零样本性能,这意味着它在与训练时不同的下游任务上表现良好,而无需进行任何微调。
CLIP 的目标是:
为什么这很重要呢?首先,许多计算机视觉模型是基于众包标注的数据集进行训练的。这些数据集通常包含数十万的样本。一些例外情况是在千万级别的样本数量。可以想象,这是一个非常耗时和昂贵的过程。另一方面,自然语言模型的数据集通常大几个数量级,并且从互联网上抓取。其次,如果一个目标检测模型已经在某些类别上进行训练,而你想添加一个额外的类别,你需要在数据中标注这个新类别并重新训练模型。
CLIP 结合自然语言和图像特征的能力,加上其零样本性能,已经导致许多其他流行基础模型的广泛采用,例如UnCLIP、EVA、SAM、稳定扩散、GLIDE或VQGAN-CLIP等。
方法
现在让我们深入探讨 CLIP 的方法。下面的图 1 展示了 CLIP 的架构以及其训练过程。
图 1 — CLIP 的架构和训练过程。图片来源 + Sascha Kirch的注释
模型架构由两个编码器模型组成,每个模态一个。对于文本编码器使用了一个变换器,而图像编码器则使用了ViT (Vision Transformer)或某个版本的 ResNet。一个针对每个模态的学习线性变换将特征转换为匹配大小的嵌入向量。最后,计算对立模态的每对嵌入向量之间的余弦相似度,并通过一个学习到的温度标量进行缩放。在训练过程中,匹配对之间的余弦相似度被最大化,而不正确对的相似度被最小化,因此框架名称中使用了“对比”一词。
除了大数据集之外,还有一些对成功至关重要的细节。首先,对比学习方法强烈依赖于批次大小 N。提供的负样本越多,学习信号越强。CLIP 的训练批次大小为 32,768,这相当大。其次,CLIP 并不是学习精确的措辞匹配,而是一个更简单的代理任务,只需学习整体文本,也称为词袋(BoW)。
有趣的事实: 使用 ResNet50x64 作为图像编码器的 CLIP 版本在 592 台 V100 GPU 上训练了 18 天,而使用 ViT 模型的版本则在 256 台 V100 GPU 上训练了 12 天。换句话说,在单个 GPU 上分别超过 29 年和超过 8 年(忽略使用不同批次大小的事实)。
一旦模型训练完成,它可以用于对图像进行对象分类。问题是:如何使用一个没有被训练来分类图像或输入类别标签而是文本提示的模型进行分类?图 2. 显示了如何操作:
图 2 — CLIP 的图像分类架构。图片来源 + Sascha Kirch 的注释
类别标签可以视为由单个词形成的文本提示。为了告知模型可用于分类任务的类别,一组 N 个类别被输入到模型中。这相比于在固定标签集上训练的分类模型具有巨大优势。我们现在可以输入 3 个类别或 100 个类别;这由我们决定。正如我们后面将看到的,为了提高 CLIP 的性能,类别标签会被转化为提示,以提供更多上下文。每个提示随后被输入到文本编码器中,并转换为嵌入向量。
输入图像被输入到图像编码器中,以获取嵌入向量。
然后计算每对文本和图像嵌入向量的余弦相似度。对获得的相似度值应用 Softmax 以形成概率分布。最后,选择概率最高的值作为最终预测。
由萨沙·基尔赫(Sascha Kirch)撰写的论文讲解
查看列表 7 篇故事
实验与消融研究
CLIP 论文展示了大量的实验和消融研究。这里我们将介绍五个我认为对理解 CLIP 成功至关重要的实验。首先是从 CLIP 作者提出的关键发现,然后我们将深入探讨细节:
-
训练效率: CLIP 在零样本迁移方面比我们的图像描述基线更有效
-
文本输入格式: 提示工程和集成方法提升了零样本性能
-
零样本性能: 零样本 CLIP 与完全监督基线具有竞争力
-
少样本性能: 零样本 CLIP 超越了少样本线性探针
-
分布偏移: 零样本 CLIP 对分布偏移比标准 ImageNet 模型更为鲁棒
训练效率
在训练过程中,图像编码器和文本编码器是共同训练的,意味着有一个单一的训练目标并同时进行。CLIP 不仅执行对比学习方案,而且文本提示作为一个整体与给定图像进行比较,因此词序并不重要。这仅仅是一个“词袋”。短语“my name is Sascha”与“Sascha name is my”会产生相同的嵌入。
预测一组词而非准确的词及其在短语中的位置是一个更简单的代理目标。下图 3 显示了在 ImageNet 上零样本准确率与初始变换器模型训练以预测精确词、训练以预测词袋的初始变换器模型以及使用词袋进行对比学习的 CLIP 模型的训练样本数量之间的关系。
“CLIP 在零样本迁移方面比我们的图像描述基线更有效” — CLIP 作者
图 3 — 零样本效率。 图像来源 + 萨沙·基尔赫 的注释
文本输入格式
正如我们在图 2 中看到的,为了进行对象分类,类别标签被转换为文本提示。当然,这不是偶然的,因为 CLIP 对单词是完全适应的。这样做是为了利用语言的描述性,并提供上下文以解决可能的歧义。以“boxer”这个词为例,它可能是某种犬类或某种运动员。CLIP 的作者展示了文本提示的格式非常重要,它可以提升性能并提高效率。
“提示工程和集成提升了零样本性能” — CLIP 作者
图 4— 提示工程和集成与无上下文类别名称。 图片来源 + Sascha Kirch 注释
订阅 Sascha Kirch 的更新 🚀 想了解更多深度学习内容或保持最新动态……
medium.com](https://medium.com/@SaschaKirch/subscribe?source=post_page-----7770858b487d--------------------------------)
零样本性能
在另一个实验中,作者将 CLIP 的零样本图像分类性能与专门在比较数据集上训练的模型进行了比较。
“零样本 CLIP 与完全监督基线具有竞争力” — CLIP 作者
图 5— 零样本 CLIP vs. 监督基线。 图片来源 + 作者注释
少样本性能
尽管零样本预测器没有在下游任务上进行微调,但少样本检测器则进行了微调。作者对多个公开的预训练模型进行了实验,并将它们在 20 个不同数据集上的少样本性能与零样本和少样本 CLIP 进行了比较。少样本模型在每个类别的 1、2、4、8 和 16 个示例上进行了微调。
有趣的是,零样本 CLIP 的表现大致与 4 样本 CLIP 相当。
如果将 CLIP 与其他模型进行比较,必须考虑到所比较的公开模型(即 BiT、SimCLR 和 ResNet)是在不同且较小的数据集上进行预训练的,而不是 CLIP 模型。
“零样本 CLIP 优于少样本线性探针” — CLIP 作者
图 6— 少样本性能。 图片来源 + Sascha Kirch 注释
分布转移
一般来说,模型对分布变化的鲁棒性指的是它在不同数据分布的数据上表现得与在训练数据分布上的表现一样好。理想情况下,它的表现应当一样好。但在现实中,其性能会下降。
零-shot CLIP 的鲁棒性已与 ResNet101 ImageNet 模型进行了比较。这两种模型在 ImageNet 的自然分布变化上进行了评估,如图 7 所示。
“零-shot CLIP 对分布变化的鲁棒性远超标准 ImageNet 模型” — CLIP 作者
图 7 — 分布变化。 图像来源 + Sascha Kirch的注释
进一步阅读与资源
正如本文开头所提到的,CLIP 已被广泛应用于大量项目中。
以下是使用 CLIP 的论文列表:
-
[GLIDE] 朝着文本引导的扩散模型进行逼真的图像生成和编辑
如果你想深入了解实现并自行测试,以下是一些库列表:
创建一个优秀 PyPI 包的最完整指南
原文:
towardsdatascience.com/the-complete-guide-for-creating-a-good-pypi-package-acb5420a03f8
你需要了解的所有内容——无论是新手还是经验丰富的用户
·发布在 Towards Data Science ·11 分钟阅读·2023 年 3 月 27 日
--
图片来源于 Unsplash。
Python 包索引(或PyPI)是第三方Python 包的官方软件库。它是 Python 程序员获取任何预构建和可重用的软件组件的一站式商店——让我们的编码生活更加轻松和高效。一个简单的pip install
就可以轻松下载并安装任何 PyPI 包。
在对我自己的 PyPI 包进行两年的持续开发和更新后,我学到了许多希望一开始就知道的东西。在本指南中,我将分享我所有的经验和技巧,这些可以让你的 PyPI 包上传和维护变得更加成功。
本指南分为两个部分:
👉 如果你是PyPI 新手,并且这是你的第一个包,从以下部分开始:1 | 为 PyPI 准备你的包
👉 如果你已经是 PyPI 用户并且已经上传了你的包,从以下部分开始:2 | 上传和维护你的 PyPI 包
1 | 为 PyPI 准备你的包
当你将你的包上传到PyPI时,你是让任何人都可以通过pip
安装器进行安装。任何人都可以运行以下命令:
pip install your_package
并且它将被安装到他们的机器上。为了让你的代码适合 PyPI,你需要遵循一些步骤:
首先,你需要创建一个 PyPI 账户。我强烈建议启用双因素认证以增强账户安全。
其次,你需要准备包的文件夹结构并添加一些必需的文件。这些文件包括:
-
__init__.py
文件将初始化你的包。 -
一个包含你的包版本的
version.py
文件。 -
实际构建 Python 包所需的文件:
setup.py
、pyproject.toml
、setup.cfg
和MANIFEST.in
。 -
LICENSE.txt
文件。 -
以及
README.md
文件。
在添加了所有这些文件后,你的包的最终文件结构将如下所示:
.
|- your_package
| |- __init__.py
| |- your_package.py
| |- version.py
|- setup.py
|- pyproject.toml
|- setup.cfg
|- MANIFEST.in
|- LICENSE.txt
|- README.md
只需稍加想象,你的包很快就会类似于下图:
示例 PyPI 包。图片来自PyPI.org由作者提供。
一起逐个查看所需的文件,了解我们为什么需要它们,以及它们可能的样子。
1.1 | init.py
这个文件将标记你的文件夹为 Python 包,并确保在启动时你的包能被正确导入。__init__.py
文件的示例如下:
# __init__.py
from .version import __version__
from .your_package import your_package
your_package.py
是你的包的主要脚本,它将通过__init__.py
文件被导入。当然,你也可以根据需要导入多个主要脚本。
1.2 | your_package.py
如1.1节中提到的,your_package.py
是你应该已经创建的包的主要脚本。如果需要,你可以在包中包含多个脚本。
1.3 | version.py
通常,流行的 Python 包允许你通过调用.__version__
函数来查找它们的版本。例如,你可以通过执行以下代码行来查找你机器上安装的Pandas版本:
import pandas as pd
pd.__version__
> '1.5.3'
为了确保你的包也能完成相同的操作,你需要在包文件夹中包含一个version.py
文件。这个文件可以像下面这样简单:
# version.py
__version__ = "1.0.0"
1.4 | setup.py 和 pyproject.toml
setup.py
文件和pyproject.toml
文件是用于构建你的包的配置文件。pyproject.toml
是随着PEP 517和PEP 518标准引入的新型配置文件。
那么,什么时候用哪个呢?
当你想创建一个 Python 包时,首先必须构建它。有各种流行的 Python 构建系统,例如setuptools
就是其中之一。但在某些情况下,你可能会选择使用其他工具,如Poetry、Flit或Bento。当使用其他打包工具时,pyproject.toml
会替代setup.py
文件。
当使用setuptools
作为构建系统时,建议包括setup.py
文件和pyproject.toml
文件。如果你使用其他构建系统,通常只需要pyproject.toml
文件。对于本指南,我们将使用setuptools
,因此我强烈建议在你的包中包含这两个文件。有关更多详细信息,我建议查看这篇文章。
下面是一个setup.py
的模板,你可以编辑并重复使用:
# setup.py
from setuptools import setup
if __name__ == "__main__":
setup(packages = ['your_package'])
其次,下面是pyproject.toml
的模板:
# pyproject.toml
[build-system]
requires = ["setuptools >= 40.8.0", "wheel"]
1.5 | setup.cfg
这个文件是必需的,以便在构建你的包时包含元数据。例如:包的名称、作者的名字、主页 URL、支持的 Python 版本等。它还确保 PyPI 使用你的README.md
作为包描述,并引用LICENSE.txt
文件。
# setup.cfg
[metadata]
name = your_package
version = attr: your_package.version.__version__
author = FirstName LastName
author_email = youremail@email.com
keywords = tags, that, describe, your, package
# add a description of your package
description = A description of your package.
long_description = file: README.md
long_description_content_type = text/markdown
# add the url to your repository/homepage and version release
url = https://github.com/username/your-package
download_url = https://github.com/username/your-package/archive/refs/tags/v1.0.0.tar.gz
# define your license and license file
license = MIT
license_files = LICENSE.txt
classifiers =
# for a full list of classifiers see https://pypi.org/classifiers/
Development Status :: 5 - Production/Stable
Intended Audience :: Developers
Topic :: Software Development :: Build Tools
License :: OSI Approved :: MIT License
Programming Language :: Python
Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
Programming Language :: Python :: 3.10
Programming Language :: Python :: 3.11
[options]
install_requires =
# list the dependencies required by your package
numpy >=1.24.2
matplotlib >= 3.7.0
python_requires = >=3.8
1.6 | MANIFEST.in
你可能创建了一个由多个不同脚本组成的包,或者包含主脚本使用的附加文件。例如,你的包可能看起来像这样:
.
|- your_package
| |- __init__.py
| |- your_package.py
| |- **additional_script.py**
| |- version.py
| |- **additional_file.csv**
|- setup.py
|- pyproject.toml
|- setup.cfg
|- MANIFEST.in
|- LICENSE.txt
|- README.md
默认情况下,当上传到 PyPI 时,只有在__init__.py
文件中引用的脚本会被包含在上传的包中。如果我们希望包含其他脚本或文件,需要在此处进行引用:
# MANIFEST.in
include your_package/addtional_script.py
include your_packager/addtional_file.csv
1.7 | LICENSE.txt
这个文件将包含你分发包的许可。选择使用哪个许可取决于你,不过,确保尊重你包所依赖的包的许可。如果你使用了已经有版权的包,你可能不能对你的包进行版权保护。
对于开源软件,常用的许可是 MIT。你可以在这里找到 MIT 许可内容的示例。
1.8 | README.md
README.md
文件可以用作你包的描述(即 PyPI 页面内容)。在本指南中,我们使用基于 Markdown 的README
,但你可以选择任何支持的标记语言。如果你决定更改标记类型,请确保在setup.cfg
文件中调整long_description_content_type
变量。
你可以使用与 GitHub 仓库中相同的README
,但要注意 PyPI 的 Markdown 与 GitHub 的 Markdown 不完全相同,可能在使用表情符号或徽章时会遇到格式问题。
在添加所有必需的文件后,你的包已经准备好上传到 PyPI了!🚀
2 | 上传和维护你的 PyPI 包
如果你的包已经完全更新并准备好发布到 PyPI,那么接下来的部分正适合你。下面你将找到一些避免常见错误和陷阱的提示和建议。应用这些建议将使你的 PyPI 包的上传和更新周期更加顺畅,最终效果更好。
2.1 | 首先将你的包上传到 TestPyPI
要提交你的包到 PyPI,你需要先构建它,然后使用Twine包来上传它。Twine 可以通过 pip
安装程序轻松安装,如下所示:
pip install twine
但是,在将任何东西上传到 PyPI 之前,我强烈建议首先将你的包上传到TestPyPI。为什么?因为一旦上传到 PyPI:
-
你不能编辑已上传的内容。这意味着你不能修复包元数据、代码、包描述(即 README)中的问题等。错误和拼写错误将保留,直到你在下一次上传时修复它们。
-
更糟糕的是:包的版本不能重新上传。例如,一旦你上传了版本 1.0.0,即使你从 PyPI 中删除了该版本,你也不能再重新上传版本 1.0.0 —— 永远不能。
TestPyPI 允许你上传并测试你的包,在实际上传到 PyPI 之前。这是 PyPI 的一个完全独立的实例,意味着你需要创建一个 TestPyPI 账户。
要将包上传到 TestPyPI,在你的包文件夹中执行以下命令:
# build the package
python setup.py sdist bdist_wheel
# upload to TestPyPI
twine upload --repository-url https://test.pypi.org/legacy/ dist/*
这将上传你的包的源分发版和构建分发版(更多细节请参见第 2.2 节)。
在完成测试和检查后,通过执行以下命令将你的包上传到 PyPI:
# build the package
python setup.py sdist bdist_wheel
# upload to PyPI
twine upload dist/*
我花了相当长的时间才发现 TestPyPI,自那时起,我再也离不开它 —— 我相信你也会如此。
2.2 | 与你的包一起上传 Wheels
我注意到在线指南中“如何上传包到 PyPI”经常显示以下命令:
# create a source distribution
python setup.py sdist
# upload to PyPI
twine upload dist/*
这将上传你的包的源分发版到 PyPI,供通过 pip
安装程序下载和安装。
除了上传源分发版,我强烈推荐同时上传构建分发版(即 Python wheel)。因为 wheel 通常体积更小且安装时无需从源分发版重新构建,所以对用户来说安装时间更快。
PyPI 包文件。图片来源于PyPI.org作者。
要创建你包的构建分发版,你首先需要安装Wheel包:
pip install wheel
然后你可以执行以下命令创建源分发版和构建分发版,并将它们上传到 PyPI:
# create a source and a built distribution
python setup.py sdist bdist_wheel
# upload to PyPI
twine upload dist/*
可以专门为特定的 Python 版本和操作系统类型构建 Wheels。上述命令将创建一个 Python 3 操作系统独立的 wheel。然而,对于你的使用场景,你可能需要添加多个 wheel 类型:例如,如果你的包应该支持 Python 2,你可以包括一个 Python 2 的 wheel。有关 wheel 的更多信息,我建议查看以下文章。
2.3 | 正确版本化你的包
每次对你的包进行更改时,确保将其上传到新版本发布。版本管理你的包非常重要,因为你的用户将能够跟踪你所做的更改,并在必要时更新他们的包安装。不要在现有版本中进行更新,因为这不是最佳实践。
以下是你可以遵循的实施正确版本管理的几个步骤:
-
定义一个版本控制语法并保持一致。例如:从 1.0.0 开始。如果你对包进行了一些小的更改,发布 1.0.1。如果你进行了一次重大更改,发布 1.1.0,等等。
-
在你的
version.py
文件中更新版本(如第1.3节所述) -
在 GitHub(或你为你的包使用的任何其他源代码仓库)上创建一个新版本发布并添加发布说明(添加/删除了哪些功能?你修复了哪些 bug?)。
-
更新
download_url
变量 在setup.cfg
文件中(如第1.5节所述),将其指向 GitHub 上新发布的tar.gz
包的 URL(或你为包使用的任何其他源代码仓库)。 -
最后:将你的新版本上传到 PyPI(但首先:测试 PyPI!)
这些步骤对你来说可能会有所不同,但如果你一直遵循了第一部分的指南,那么它们将包含所有实施你包的正确版本管理所需的操作。
2.4 | 记录你的包
在为公众使用创建包时,重要的是创建结构清晰的文档,并且如果你对代码做出任何更改,始终更新文档。
一个包的好坏取决于其文档的质量。
所需的文档量取决于你的包的复杂性:一个简单的包可能只需要几行说明,这些说明可以直接包含在仓库的README
中。如果你的包更复杂,README
可能会显得过于有限。
如果我可以重新开始,我会从第一天起使用 readthedocs.org 。Readthedocs 是一个流行的软件文档托管平台。你会发现许多流行的 Python 包都在那儿托管他们的文档(见 Pandas 或 Pillow)。
托管在 readthedocs 上有各种好处,包括:
-
自动创建可下载格式的文档,例如 PDF 或 HTML 格式。
-
支持文档版本控制,
-
遵循用户通常已经熟悉的标准页面结构。
你可以将 readthedocs 连接到你的 GitHub 仓库,这样你可以直接在本地机器上编辑文档。然后,每当你向 GitHub 仓库提交更改时,它将立即触发文档的新构建。
我推荐你参考这篇官方教程来了解如何设置 readthedocs。作为参考示例,这是我在GitHub上的readthedocs 文档 difPy 包。
2.5 | 维护代码卫生
最后,在将你的包发布到 PyPI 时,保持一定的代码卫生标准也非常重要。源代码的质量越高,你的包就会越可信。
一些代码卫生的最佳实践包括:
-
一致使用变量和函数名称:要么使用首字母大写的变量名,例如
variableName
,要么使用下划线分隔的变量名,如variable_name
。不要在一个脚本中混合使用这两种方式。 -
一致使用引号:Python 支持单引号
'
和双引号"
。选择在你的脚本中使用哪一种,并且不要混用(除非绝对必要,例如在格式化字符串时)。 -
注释你的代码!这对你来说可能很明显,但作为最佳实践必须包含在内。
当然,除了我在这里列出的最佳实践,还有更多的实践可以遵循,因此我建议你查看这篇文章以获取更多信息。
结论
上传和维护 PyPI 包可以非常有趣且充满成就感。通过让其他人使用你的工作来分享你的成果,帮助你学习并通过获得来自 Python 社区的反馈和改进建议来获得更多编码经验。
我希望本指南为你提供了一些宝贵的见解,帮助你开始使用 PyPI——或者如果你已经在使用 PyPI,能给你一些新的提示来改进你当前的使用方式。✨
如果你有任何反馈,请告诉我,也可以分享你自己的 PyPI 使用技巧,留下评论吧!🍀
参考文献
[1] PyPA, 打包 Python 项目 (2023)
[2] Setuptools, 使用 pyproject.toml 文件配置 setuptools (2023)
[3] Readthedocs, Read the Docs:简化文档 (2023)
Python 中的生存分析完全介绍
原文:
towardsdatascience.com/the-complete-introduction-to-survival-analysis-in-python-7523e17737e6
了解生存分析、其在行业中的应用以及如何在 Python 中应用它
·发表于Towards Data Science ·13 min 阅读·2023 年 5 月 9 日
--
图片由Ricky Kharawala提供,来自Unsplash
生存分析是一个迷人的领域,但在数据科学从业者中很少被使用或提及,至少在我的经验中是这样。虽然它主要用于医疗保健行业,但生存分析也可以应用于各种领域。
本文的目标是让你发现生存分析及其在你所在行业中的潜在应用。
在本文中,我们首先定义生存分析,并阐述其在不同领域的应用动机。然后,我们将了解如何实现生存分析的不同算法,并学习如何评估它们。
让我们开始吧!
什么是生存分析?
生存分析是统计学的一个分支,测量事件发生的预期持续时间。这个名称来自临床研究,因为我们对患者的生存感兴趣,换句话说,就是延长生存时间。
生存分析根据应用领域的不同可以有许多不同的名称。在工程学中,我们称之为可靠性分析,在经济学中称为持续时间分析。时间到事件分析也是另一个常见的名称。
回归还是分类?
既然生存分析测量事件发生的时间长度,这意味着它是一种回归类型。
然而,生存分析的输出不是一个连续的数字。
相反,我们的目标是生成生存函数或风险函数。
生存函数表示事件未发生的概率与时间的函数关系。
另一方面,风险函数表示事件在某个时间点发生的概率。
当我们实现不同的生存分析模型时,我们将更详细地查看这些函数。
什么时候使用生存分析?
现在我们了解到生存分析测量的是事件发生的时间,我们可以看到这并不专属于医疗保健领域。
实际上,生存分析可以在我们需要在事件发生之前采取行动的任何时候使用。
以员工流失问题为例。假设我们有以下数据集。
+--------+----------------+-------------------+----------------+----------+
| Salary | Years employed | Nb. of promotions | Direct manager | Has left |
+--------+----------------+-------------------+----------------+----------+
| 75000 | 2 | 1 | John Doe | 0 |
| 105000 | 4 | 2 | Jane Doe | 0 |
| 40000 | 2 | 0 | John Doe | 1 |
+--------+----------------+-------------------+----------------+----------+
直观地,我们将其框定为一个二分类问题,其中员工要么留在公司,要么离开。
如果我们的模型预测员工会留下,他们会待多久?如果模型预测员工会离开,他们会在多久之前离开?作为一个分类问题,要么我们不知道这些问题的答案,要么我们需要一个固定的时间段。例如,我们的数据标记为员工在 6 个月内离职。
将员工流失视为生存分析问题,我们可以将员工留在公司的概率作为时间的函数。这样,我们可以在员工实际离职之前采取行动。此外,我们可以分析我们的特征,并确定每个特征如何影响员工的离职时间。
现在,这只是一个例子,但生存分析可以应用于更多的情况,比如:
-
设备在多久之前需要维护?
-
客户在多久之前会偿还债务?
-
软件故障在多久之前会得到解决?
-
客户在多久之前会进行下一次购买?
家庭作业的核心信息是,当我们不仅对事件发生感兴趣,而且对事件发生前的时间持续性感兴趣时,我们应该使用生存分析。
生存分析中的数据
由于生存分析关心的是事件的发生及其发生前的时间持续性,我们的数据必须包含这些信息。
让我们重新查看员工流失的样本数据集。
+--------+----------------+-------------------+----------------+----------+
| Salary | Years employed | Nb. of promotions | Direct manager | Has left |
+--------+----------------+-------------------+----------------+----------+
| 75000 | 2 | 1 | John Doe | 0 |
| 105000 | 4 | 2 | Jane Doe | 0 |
| 40000 | 2 | 0 | John Doe | 1 |
+--------+----------------+-------------------+----------------+----------+
在这里,我们可以看到我们拥有进行生存分析所需的信息:我们有雇佣年限以及员工是否离开。
当然,我们还有关于尚未离职员工的数据。
这就是我们所说的删失数据。对于两个员工,我们不知道他们何时会离开公司,或者他们是否会离开。
不过,我们不会等到所有员工离职后再采取行动,因此生存分析特别设计用于处理删失数据。
删失数据的类型
删失数据有三种类型:
-
右删失
-
左删失
-
间隔删失
让我们看看每种类型。
右删失数据
对于右删失数据,我们知道每个受试者的起始点,但并非每个人的事件都被观察到,如下所示。
右删失数据:我们知道起始点,但不知道事件是否发生在每个受试者身上。图片来源:作者。
在上图中,受试者 A 离开了公司。对于受试者 B,在实验期间,他们留在公司。然而,我们不能知道他们是否在之后离开。因此,这是右删失的。
受试者 C 是另一个右删失数据的例子,其中某人简单地离开了实验。这可能发生在临床试验中,当某人仍然活着但离开试验时,我们就失去了对他们的跟踪。
左删失数据
要理解左删失数据,我们可以考虑感染病毒的例子。
在这里,左删失数据意味着我们不知道个人何时被感染,但我们知道他们有病毒。
左删失数据:我们不知道事件发生的时间,但后来观察到它。图片来源:作者。
在上图中,事件发生在我们观察之前,但我们无法知道确切时间。不过,这种类型的数据仍然可以用于生存分析。
区间删失数据
最后,区间删失数据是指事件发生在两个观察时刻之间,但我们不知道确切的时间。
区间删失数据:事件发生在两个观察点之间。图片来源:作者。
为了理解这一点,假设你去看医生并测试病毒。在第一次访问时,结果是阴性,你没有感染。
然后,下一周,你回到医生那里并测试病毒。现在,测试结果是阳性,这意味着你在两次访问之间的某个时候被感染了,但我们不知道确切的时间。
应用生存分析
现在我们对生存分析有了总体理解,包括其应用以及可以处理的数据类型,让我们用一个例子实际应用生存分析。
这里,我们使用的是 1980 年的退伍军人管理局肺癌试验数据集(Kalbfleisch, J.D., Prentice, R.L.:《失败时间数据的统计分析》。John Wiley & Sons, Inc. (2002))。数据集可以公开下载。
这个数据集包含 137 名患者的数据,具有变量,并且是右删失的。这意味着在研究过程中,并不是所有患者都去世了。
这里,这个实验有两个主要目标:
-
不同的治疗方法能否改善生存时间?
-
我们能否预测患者的生存时间?
为了回答第一个问题,让我们实现我们的第一个生存分析方法:Kaplan-Meier 估计器。
实施 Kaplan-Meier 估计器
Kaplan-Meier 估计器是一种非参数统计方法,用于估计生存函数,它在处理右删失数据时效果良好。
正如我们很快会看到的,Kaplan-Meier 估计器将生成一个生存函数,表现为一系列下降的水平阶梯,如同楼梯一样。给定足够的样本,这个函数将接近真实的生存函数。
Kaplan-Meier 估计器的主要假设是,被删失的数据与未删失的数据具有相同的生存概率。因此,如果有人离开实验,我们将假设他们的生存概率与那些留在实验中并被观察到的人的生存概率相同。
为了实现这一点,我们将使用scikit-survival Python 包。这是一个包含数据集和常见生存分析模型的库。
首先,让我们读取数据。
from sksurv.datasets import load_veterans_lung_cancer
X, y = load_veterans_lung_cancer()
很好!现在,我们可以导入 Kaplan-Meier 估计器并生成生存曲线。为此,我们需要传入两个参数:患者的状态(去世或未去世)和事件的时间(从研究开始到观察的天数)。
from sksurv.nonparametric import kaplan_meier_estimator
time, survival_prob = kaplan_meier_estimator(y['Status'], y'Survival_in_days
然后,我们可以绘制生存函数。
fig, ax = plt.subplots()
ax.step(time, survival_prob, where='post')
ax.set_ylabel('Probability of survival')
ax.set_xlabel('Time')
plt.tight_layout()
![
Kaplan-Meier 估计器在退伍军人管理局肺癌试验数据集上的生存函数。注意曲线如何如预期般是一个系列下降的水平阶梯。图片由作者提供。
从上面的图形中,我们可以看到生存函数确实是一系列如预期的下降水平阶梯。当研究开始时,大多数患者仍然活着,因此生存概率很高。然后,曲线迅速下降,意味着大多数患者在研究的前 400 天内去世。
现在,可以为不同的患者组生成生存函数。例如,在数据集中,进行了两种不同的治疗。了解某种治疗是否提高了生存机会将是有趣的。
让我们自己看看!我们将数据分为两组,根据治疗类型生成生存函数。然后,我们绘制它。
for treatment_type in ('standard', 'test'):
mask_treatment = X['Treatment'] == treatment_type
time, survival_prob = kaplan_meier_estimator(
y['Status'][mask_treatment],
y['Survival_in_days'][mask_treatment]
)
plt.step(time, survival_prob, where='post', label=f'{treatment_type}')
plt.ylabel('Probability of survival')
plt.xlabel('Time')
plt.legend(loc='best')
plt.tight_layout()
两种不同治疗类型的生存函数。图片由作者提供。
从上面的图形来看,实验治疗似乎产生了比标准治疗更持久的生存曲线。
但是这种差异重要吗?
为了回答这个问题,我们使用logrank 检验。这是一个统计检验,用于确定两个生存曲线是否显著不同。在这里,零假设声明两条曲线没有显著差异。
使用scikit-survival,我们可以运行检验并输出 p 值。
from sksurv.compare import compare_survival
group_indicator = X.loc[:, 'Treatment']
groups = group_indicator.unique()
chi2, pvalue= compare_survival(y, group_indicator)
print(pvalue)
这会输出一个 0.93 的 p 值。由于它不小于 0.05,我们不能拒绝零假设,并得出结论,生存曲线没有显著差异。因此,治疗类型没有帮助患者活得更久。
带协变量的生存分析
Kaplan-Meier 估计器是一个很好的起点,但由于它是一个非参数模型,无法考虑数据集中的任何特征。因此,我们将注意力转向可以考虑特征来估计生存函数的模型。
首先,让我们对数据进行独热编码,以便特征可以被使用。
from sksurv.preprocessing import OneHotEncoder
X_num = OneHotEncoder().fit_transform(X)
编码后的特征。图片由作者提供。
太好了!现在,我们可以应用考虑协变量的模型来评估生存情况。在这里,我们使用 Cox 比例风险模型。
Cox 比例风险模型
Cox 比例风险模型是评估不同因素对生存影响的模型之一。这样,我们可以确定哪些因素可以改善生存,哪些因素减少生存机会。
在这里,模型实际上估计了风险函数。换句话说,它计算了在某一点发生事件的概率。这与生存函数相反,生存函数估计在某一点事件不发生的概率。
可视化风险和生存函数。风险函数随着时间增加,而生存函数随着时间减少。图片由 Marta Sestelo 提供,来源于 《生存分析简短课程》。
从上图可以看出,生存函数随着时间减少,而风险函数随着时间增加。当然,一旦我们有了一个函数,我们可以轻松计算另一个函数。
因此,Cox 模型是一个表达为风险函数的模型:
Cox 比例风险方程。图片由作者提供。
在这里,h 代表风险(事件发生的概率),协变量由 x 表示。然后,系数 b 可以用来解释每个协变量的影响:
-
如果 b = 0,那么该特征没有影响
-
如果 b > 0,那么这些特征增加了风险(因此生存率下降)
-
如果 b < 0,那么该特征会降低风险(因此生存率增加)
该模型的一个重要假设是风险是成比例的,并且这种比例与时间无关。因此,如果在某个初始时间点,A 主体的死亡风险是 B 主体的两倍,那么这种比例保持不变,无论时间如何变化。
应用 Cox 比例风险模型
现在我们理解了 Cox 模型,让我们在数据集上应用它。
我们只需初始化模型并在数据上进行拟合。
from sksurv.linear_model import CoxPHSurvivalAnalysis
estimator = CoxPHSurvivalAnalysis()
estimator.fit(X_num, y)
然后,对于未见患者的数据,模型可以为每个患者生成生存函数。注意使用 predict_survival_function 方法来获得生存函数,而不是风险函数。
# Create a set of 4 synthetic patients
X_test = pd.DataFrame.from_dict({
1: [65, 0, 0, 1, 60, 1, 0, 1],
2: [65, 0, 0, 1, 60, 1, 0, 0],
3: [65, 0, 1, 0, 60, 1, 0, 0],
4: [65, 0, 1, 0, 60, 1, 0, 1]},
columns=X_num.columns, orient='index')
# Estimate the survival functions
pred_surv = estimator.predict_survival_function(X_test)
# Plot the survival function for each new patient
time_points = np.arange(1, 1000)
for i, surv_func in enumerate(pred_surv):
plt.step(time_points, surv_func(time_points), where='post', label=f'Sample {i+1}')
plt.ylabel('Probability of survival')
plt.xlabel('Time')
plt.legend(loc='best')
plt.tight_layout()
每个新患者的 Cox 比例风险模型的生存函数。图片由作者提供。
从上图中,我们可以看到模型为每个患者生成了一个独特的生存函数。我们可以看到,对于样本 4,生存时间减少最快,而样本 2 减少最慢。
评估生存分析模型
所以,我们从我们的 Cox 比例风险模型中获得了预测并得到了生存曲线。但是我们怎么知道这些预测是否有效呢?
生存分析中的常见评估指标是 一致性指数 或 c-index 和时间依赖的 ROC AUC。让我们更详细地探讨这两者。
一致性指数(c-index)
生存模型将预测危险概率。因此,具有较高危险概率的样本应具有较短的生存时间。
然后,为了计算 c-index,我们取一对样本并查找以下内容:
-
如果对中的两个样本被截尾,则该对样本被忽略(对 c-index 无影响)
-
如果具有更高预测危险的样本比对中其他样本的生存时间更短,那么它就是一个一致对(c-index 增加)
-
如果具有更高预测危险的样本比对中其他样本的生存时间更长,那么该对样本是不同意对(c-index 减少)。
然后,为了解释 c-index,我们使用与分类中 ROC AUC 相同的逻辑:
-
0.5 是一个随机模型
-
1.0 是一个完美模型
-
0 是一个每次都错的模型
因此,你希望你的 c-index 至少大于 0.5,并且越接近 1 越好。
使用 c-index 评估我们的 Cox 模型
现在,让我们计算我们 Cox 模型的 c-index。
from sksurv.metrics import concordance_index_censored
estimator.score(X_num, y)
这返回一个 0.74 的 c-index,意味着我们的模型比随机模型表现更好,这是一个好兆头。
时间依赖的 ROC AUC
如果你曾处理过分类问题,你可能遇到过 ROC AUC 作为评估指标。你基本上是通过测量 ROC 曲线下的面积来评估模型性能。再次强调,你希望该面积大于 0.5,并尽可能接近 1。
现在,在生存分析中,我们有一个连续的结果,意味着 ROC 随时间变化,这与二元分类不同。例如,一个客户可以按时还信用卡,但随后在未来某个时点开始违约。
因此,使用时间依赖的 ROC AUC 对于评估模型在时间 t 发生事件的能力是有用的。
使用动态 ROC AUC 评估我们的 Cox 模型。
使用 scikit-survival,让我们通过时间依赖的 ROC AUC 来评估我们的 Cox 模型。
from sklearn.model_selection import train_test_split
# Split into train/test
X_train, X_test, y_train, y_test = train_test_split(X_num, y, test_size=0.2, stratify=y['Status'], random_state=42)
# Fit the model on the training set
cph = CoxPHSurvivalAnalysis()
cph.fit(X_train, y_train)
# Plot the time-depdendent ROC and calculate AUC
from sksurv.metrics import cumulative_dynamic_auc
time_interval = np.arange(8, 184, 7)
cph_risk_scores = cph.predict(X_test)
cph_auc, cph_mean_auc = cumulative_dynamic_auc(y_train, y_test, cph_risk_scores, time_interval)
fig, ax = plt.subplots()
ax.plot(time_interval, cph_auc, marker='o')
ax.axhline(cph_mean_auc, ls='--')
ax.set_xlabel('Days after enrollment')
ax.set_ylabel('Time-dependent AUC')
plt.grid(True)
plt.tight_layout()
我们 Cox 模型的时间依赖 ROC AUC。平均 AUC 为 0.85。图片由作者提供。
如上图所示,我们的模型性能随时间变化。我们看到其最佳性能在 75 到 125 天之间。因此,我们的模型在中期预测效果最好。
从这里开始,我们可以开发其他生存模型,以查看它们在短期或长期内预测事件的表现如何,从而补充 Cox 模型。
结论
恭喜你完成了这篇文章!信息量很大,尤其是如果你对生存分析完全陌生的话。
希望你学到了新的东西,并且我激发了你去探索生存分析领域,并将其应用于你的项目!
让我们在LinkedIn上保持联系!
干杯 🍻
支持我
喜欢我的工作吗?通过Buy me a coffee来支持我,这是一种简单的方式来鼓励我,同时我也能享受一杯咖啡!如果你愿意,请点击下面的按钮 👇
将大型语言模型(LLMs)集成到应用中的复杂性与挑战
计划将某些 LLM 服务集成到您的代码中吗?这里有一些在进行集成时可能遇到的常见挑战
·
关注 发布于 Towards Data Science ·7 分钟阅读·2023 年 7 月 8 日
--
图片由 Christina @ wocintechchat.com 提供,来源于 Unsplash
在 OpenAI 的 ChatGPT 和 GPT API 发布之前,大型语言模型(LLMs)已经存在。但由于 OpenAI 的努力,GPT 现在对开发者和非开发者都易于获取。这次发布无疑在 AI 的近期复兴中发挥了重要作用。
OpenAI 的 GPT API 在推出仅六个月内就被迅速接受,确实令人惊叹。几乎所有 SaaS 服务都以某种方式整合了它,以提高用户的生产力。
然而,只有那些完成了此类 API 设计和集成工作的人,才能真正理解其中的复杂性和新挑战。
在过去几个月里,我实现了几个利用 OpenAI GPT API 的功能。在这个过程中,我遇到了几个似乎对任何使用 GPT API 或其他 LLM API 的人都很常见的挑战。通过在这里列出这些挑战,我希望能帮助工程团队正确准备和设计他们的 LLM 功能。
让我们来看一些典型的障碍。
上下文记忆与上下文限制
这可能是所有挑战中最常见的。LLM 输入的上下文是有限的。就在最近,OpenAI 发布了对 16K tokens 的上下文支持,在 GPT-4 中,上下文限制可以达到 32K,这相当于几页(例如,如果你希望 LLM 处理一个包含几页的大文档)。但在许多情况下,你需要更多,尤其是当处理许多文档,每个文档有几十页时(比如想象一下一个需要处理数十份法律文档以提取答案的法律技术公司)。
有不同的技术来克服这个挑战,还有其他技术正在出现,但这意味着你必须自己实现这些技术中的一种或多种。又一项需要实现、测试和维护的工作负担。
数据增强
你的基于 LLM 的功能可能需要某种专有数据作为输入。无论你是将用户数据作为上下文的一部分,还是使用你存储的其他收集数据或文档,你都需要一个简单的机制来抽象从你拥有的各种数据源中获取数据的调用。
模板化
你提交给 LLM 的提示将包含硬编码的文本和来自其他数据源的数据。这意味着你将创建一个静态模板,并在运行时动态填充数据,这些数据应作为提示的一部分。换句话说,你将为你的提示创建模板,并且可能有多个。
这意味着你应该使用某种模板框架,因为你可能不希望你的代码看起来像一堆字符串拼接。
这不是一个大挑战,但另一个需要考虑的任务。
测试与微调
使 LLM 达到令人满意的准确度需要大量的测试(有时这仅仅是通过大量的试错进行提示工程)和基于用户反馈的微调。
当然,还有作为 CI 部分运行的测试,以确保所有集成都正常工作,但这不是实际的挑战。
当我说测试时,我是指在沙盒中反复运行提示,以微调结果的准确性。
对于测试,你会需要一种方法,使测试工程师可以更改模板,用所需的数据丰富它们,并执行提示以测试我们是否得到了想要的结果。你如何设置这样的测试框架?
此外,我们需要通过获取用户对 LLM 输出的反馈来不断微调 LLM 模型。我们如何设置这样的过程?
缓存
LLM 模型,如 OpenAI 的 GPT,有一个参数可以控制答案的随机性,从而让 AI 更具创造力。然而,如果你处理大规模的请求,你将会产生高昂的 API 调用费用,可能会遇到速率限制,并且你的应用性能可能会下降。如果 LLM 的一些输入在不同调用中重复出现,你可能需要考虑缓存答案。例如,你处理了 100K 次调用你的 LLM 基础功能。如果所有这些调用都触发对 LLM 提供商的 API 调用,那么成本将非常高。然而,如果输入重复出现(这可能在你使用模板并填入特定用户字段时发生),那么你有很高的机会可以保存一些预处理过的 LLM 输出,并从缓存中提供服务。
这里的挑战是为此构建一个缓存机制。这并不难实现;它只是增加了另一层需要维护和正确处理的部件。
安全性和合规性
安全性和隐私可能是这个过程最具挑战性的方面——我们如何确保创建的过程不会导致数据泄露,以及我们如何确保没有个人身份信息被暴露?
此外,你还需要审计所有操作,以确保所有操作都可以检查,以确保没有数据泄露或隐私政策的违反。
这是任何依赖第三方服务的软件公司常见的挑战,也需要在这里解决。
可观察性
与任何你使用的外部 API 一样,你必须监控其性能。是否有错误?处理时间多长?我们是否超过或即将超过 API 的速率限制或阈值?
此外,你还需要记录所有调用,不仅仅是为了安全审计目的,还为了通过评分输出来帮助你微调 LLM 工作流或提示。
工作流管理
假设我们开发了一款律师用来提高生产力的法律技术软件。在我们的例子中,我们有一个基于 LLM 的功能,它从 CRM 系统中提取客户的详细信息和案件的一般描述,并基于法律先例为律师的查询提供答案。
让我们看看完成这些任务需要做些什么:
-
根据给定的客户 ID 查找所有客户的详细信息。
-
查找当前正在处理的案件的所有详细信息。
-
使用 LLM 根据律师的查询从当前正在处理的案件中提取相关信息。
-
将上述所有信息组合到一个预定义的问题模板中。
-
用大量法律案件丰富上下文。(回忆一下上下文记忆挑战)
-
让 LLM 找到最匹配当前案件、客户和律师查询的法律先例。
现在,想象一下你有 2 个或更多这样的功能,最后试着想象在实现这些工作流之后你的代码会是什么样的。我敢打赌,仅仅想到这里的工作就让你在椅子上感到不舒服。
为了使你的代码具有可维护性和可读性,你需要实现各种抽象层,并且如果你预见到未来会有更多工作流,可能需要考虑采用或实现某种工作流管理框架。
最后,这个例子引出了下一个挑战:
强耦合代码
现在你已经意识到上述所有挑战及其复杂性,你可能会开始看到一些需要完成的任务不应由开发人员负责。
具体来说,所有与构建工作流、测试、微调、监控结果和外部 API 使用相关的任务可以由更专注于这些任务且专长不在于软件开发的人来完成。我们称这个角色为LLM 工程师。
没有理由将 LLM 工作流、测试、微调等工作放在软件开发人员的职责范围内——软件开发人员擅长构建软件。同时,LLM 工程师应该擅长构建和微调 LLM 工作流,而不是开发软件。
但目前的框架中,LLM 工作流管理与代码库耦合在一起。任何构建这些工作流的人都需要具备软件开发人员和 LLM 工程师的专业知识。
有一些方法可以进行解耦,例如创建一个处理所有工作流的专用微服务,但这又是另一个需要解决的挑战。
这些只是一些挑战。
我没有深入探讨如何解决所有这些问题,因为我自己还在努力弄清楚。不过,我可以说,LangChain似乎是唯一一个在某种程度上接近解决这些问题的框架,虽然距离完全解决还远,但方向似乎是对的。随着时间的推移,我相信 LLM 提供商会改进他们的产品,并提供能够在一定程度上应对这些挑战的平台。
如果你有任何额外的挑战要分享,请在评论中告诉我们,以便其他读者受益。此外,如果你知道任何可以帮助应对这些挑战的工具,也请在评论中分享。
希望你觉得这篇文章有启发!如果有,请通过鼓掌和关注我来表达你的支持,以便获取更多关于团队建设、软件工程和技术的文章。
实体解析实现的复杂性
关于数据匹配时一些典型挑战的实用示例
·
关注 发布于 Towards Data Science ·11 min read·2023 年 8 月 14 日
--
实体的艺术表现(图像由作者提供)
实体解析是确定数据集中两个或更多记录是否指向同一现实世界实体的过程,通常是一个人或公司。乍一看,实体解析可能看起来是一个相对简单的任务:例如,给出一个人的两张照片,即使是小孩也可以以相当高的准确率确定是否显示的是同一个人。计算机也是如此:比较包含姓名、地址、电子邮件等属性的两个记录是很容易做到的。然而,越深入探讨这个主题,挑战越大:需要评估各种匹配算法,处理数百万或数十亿条记录意味着平方复杂度,更不用说实时和数据删除的使用案例了。
模糊文本匹配
让我们开始比较著名艺术家 Vincent Van Gogh 的两个记录——还是 Van Gough?
第二条记录中有一些错误(除了出生在一个世纪之后和一个电子邮件地址):名字拼写错误,出生日期混淆,邮政编码缺失,电子邮件地址略有不同。
那么我们如何比较这些值呢?如果,假设姓名相同,那么对这些值进行简单的字符串比较就足够了。由于情况并非如此,我们需要一些更先进的模糊匹配方法。文本基础的模糊匹配有许多不同的算法,大致可以分为三组。语音算法关注文本发音的相似性。最著名的算法是 Soundex 和 Metaphone,这些算法主要用于英语文本,但也有适用于其他语言的变体,如适用于德语的 Kölner Phonetik(科隆语音)。文本距离算法通常定义了将一个文本更改为另一个文本需要改变多少个字符。Levenshtein 和 Hamming 距离是这一组中两个知名的算法。相似性算法,如余弦相似性或 Jaccard 指数,计算文本的结构相似性,并通常以百分比表示相似性。
出于本文的目的,我们将使用一种非常简单的方法,仅对姓名使用 Levenshtein 距离,对城市进行等值比较。此示例及所有后续示例将使用 golang 作为编程语言,并尽可能使用现有库。将其转换为 Python、Java 或任何其他语言应该是微不足道的。此外,它仅对姓名属性进行匹配。添加更多属性或使其可配置不是本文的目的。
package main
import (
"fmt"
"github.com/hbollon/go-edlib"
)
type Record struct {
ID int
Name string
City string
}
func matches(a, b Record) bool {
distance := edlib.LevenshteinDistance(a.Name, b.Name)
return distance <= 3 && a.City == b.City
}
func main() {
a := Record{
Name: "Vincent Van Gogh",
City: "Paris",
}
b := Record{
Name: "Vince Van Gough",
City: "Paris",
}
if matches(a, b) {
fmt.Printf("%s and %s are probably the same person\n", a.Name, b.Name)
} else {
fmt.Printf("%s and %s are probably not the same person\n", a.Name, b.Name)
}
}
在 Go Playground 中尝试:go.dev/play/p/IJtanpXEdyu
两个名字之间的 Levenshtein 距离为 3。原因是有三个额外的字符(名字中的“en”和姓氏中的“u”)。注意,这适用于这个特定输入。然而,距离仍然远未完美。例如,“Joe Smith”和“Amy Smith”之间的 Levenshtein 距离也是三,但显然不是同一个人。结合距离算法和语音算法可能解决这个问题,但超出了本文的范围。
使用基于规则的方法而不是基于机器学习的方法时,选择能够为你的用例提供最佳结果的算法是商业成功的关键方面。这是你应该花费大部分时间的地方。不幸的是,正如我们现在将发现的那样,如果你决定自己开发实体解析引擎,还有很多其他事情会让你分心,妨碍你优化这些规则。
天真的实体解析
既然我们知道了如何比较两个记录,我们需要找到所有匹配的记录。最简单的方法是将每条记录与所有其他记录进行比较。为了这个例子,我们使用随机选择的名字和城市。对于名字,我们强制引入最多三个错误(将任何字符替换为 x)。
var firstNames = [...]string{"Wade", "Dave", "Seth", "Ivan", "Riley", "Gilbert", "Jorge", "Dan", "Brian", "Roberto", "Daisy", "Deborah", "Isabel", "Stella", "Debra", "Berverly", "Vera", "Angela", "Lucy", "Lauren"}
var lastNames = [...]string{"Smith", "Jones", "Williams", "Brown", "Taylor"}
func randomName() string {
fn := firstNames[rand.Intn(len(firstNames))]
ln := lastNames[rand.Intn(len(lastNames))]
name := []byte(fmt.Sprintf("%s %s", fn, ln))
errors := rand.Intn(4)
for i := 0; i < errors; i++ {
name[rand.Intn(len(name))] = 'x'
}
return string(name)
}
var cities = [...]string{"Paris", "Berlin", "New York", "Amsterdam", "Shanghai", "San Francisco", "Sydney", "Cape Town", "Brasilia", "Cairo"}
func randomCity() string {
return cities[rand.Intn(len(cities))]
}
func loadRecords(n int) []Record {
records := make([]Record, n)
for i := 0; i < n; i++ {
records[i] = Record{
ID: i,
Name: randomName(),
City: randomCity(),
}
}
return records
}
func compare(records []Record) (comparisons, matchCount int) {
for _, a := range records {
for _, b := range records {
if a == b {
continue // don't compare with itself
}
comparisons++
if matches(a, b) {
fmt.Printf("%s and %s are probably the same person\n", a.Name, b.Name)
matchCount++
}
}
}
return comparisons, matchCount
}
func main() {
records := loadRecords(100)
comparisons, matchCount := compare(records)
fmt.Printf("made %d comparisons and found %d matches\n", comparisons, matchCount)
}
在 Go Playground 中尝试:go.dev/play/p/ky80W_hk4S3
你应该看到一些类似这样的输出(如果没有匹配的随机数据,你可能需要运行多次):
Daisy Williams and Dave Williams are probably the same person
Deborax Browx and Debra Brown are probably the same person
Riley Brown and RxxeyxBrown are probably the same person
Dan Willxams and Dave Williams are probably the same person
made 9900 comparisons and found 16 matches
如果你运气好的话,你也会得到像“Daisy”和“Dave”这样的不匹配。这是因为我们使用了三的 Levenshtein 距离,而对于短名字来说,这个值太高了。欢迎你自己改进这个方法。
性能方面,真正的问题在于需要进行 9,900 次比较才能得到结果,因为输入量的增加会大约使所需的比较次数增加四倍。200 条记录需要 39,800 次比较。对于仅有 100,000 条记录的小数据量,这意味着需要近 100 亿次比较。不管你的系统多么强大,数据量增加到一定程度时,系统将无法在可接受的时间内完成。
一个快速但几乎无用的优化是不要将每个组合比较两次。A 与 B 或 B 与 A 的比较结果应无差别。然而,这只能将所需比较次数减少一半,由于二次增长,这种减少微不足道。
通过阻塞减少复杂性
如果我们查看创建的规则,会很容易发现,如果城市不同,我们将永远不会有匹配。所有这些比较都是完全浪费的,应该被避免。将你怀疑相似的记录放入一个公共桶,将其他记录放入另一个桶,在实体解析中称为阻塞。由于我们希望使用城市作为阻塞键,因此实现起来相当简单。
func block(records []Record) map[string][]Record {
blocks := map[string][]Record{}
for _, record := range records {
blocks[record.City] = append(blocks[record.City], record)
}
return blocks
}
func main() {
records := loadRecords(100)
blocks := block(records)
comparisons := 0
matchCount := 0
for _, blockRecords := range blocks {
c, m := compare(blockRecords)
comparisons += c
matchCount += m
}
fmt.Printf("made %d comparisons and found %d matches\n", comparisons, matchCount)
}
在 Go Playground 中尝试: go.dev/play/p/1z_j0nhX-tU
结果现在将是相同的,但比较次数仅为之前的十分之一,因为我们有十个不同的城市。在实际应用中,由于城市的方差更大,这一效果会更为显著。此外,每个块可以独立处理,例如,在相同或不同的服务器上并行处理。
找到合适的阻塞键本身可能是一个挑战。使用像城市这样的属性可能会导致不均匀的分布,从而导致某些巨大块(例如大城市)所需的时间比其他块要长得多。或者城市中包含一个微小的拼写错误,从而不再被视为有效匹配。使用多个属性和/或使用语音键或 q-grams 作为阻塞键可以解决这些问题,但会增加软件的复杂性。
从匹配到实体
到目前为止,我们只能说记录是否匹配。对于非常基本的用例,这可能已经足够。然而,大多数情况下,你会想知道属于同一实体的所有匹配。这可能从简单的星形模式(例如 A 与 B、C 和 D 匹配),到链式模式(例如 A 与 B 匹配,B 与 C 匹配,C 与 D 匹配),再到非常复杂的图形模式。这种所谓的传递记录链接可以使用连通组件算法轻松实现,只要所有数据都适合在单台服务器的内存中。再次强调,在现实世界的应用中,这会更具挑战性。
func compare(records []Record) (comparisons int, edges [][2]int) {
for _, a := range records {
for _, b := range records {
if a == b {
continue // don't compare with itself
}
comparisons++
if matches(a, b) {
edges = append(edges, [2]int{a.ID, b.ID})
}
}
}
return comparisons, edges
}
func connectedComponents(edges [][2]int) [][]int {
components := map[int][]int{}
nextIdx := 0
idx := map[int]int{}
for _, edge := range edges {
a := edge[0]
b := edge[1]
aIdx, aOk := idx[a]
bIdx, bOk := idx[b]
switch {
case aOk && bOk && aIdx == bIdx: // in same component
continue
case aOk && bOk && aIdx != bIdx: // merge two components
components[nextIdx] = append(components[aIdx], components[bIdx]...)
delete(components, aIdx)
delete(components, bIdx)
for _, x := range components[nextIdx] {
idx[x] = nextIdx
}
nextIdx++
case aOk && !bOk: // add b to component of a
idx[b] = aIdx
components[aIdx] = append(components[aIdx], b)
case bOk && !aOk: // add a to component of b
idx[a] = bIdx
components[bIdx] = append(components[bIdx], a)
default: // create new component with a and b
idx[a] = nextIdx
idx[b] = nextIdx
components[nextIdx] = []int{a, b}
nextIdx++
}
}
cc := make([][]int, len(components))
i := 0
for k := range components {
cc[i] = components[k]
i++
}
return cc
}
func main() {
records := loadRecords(100)
blocks := block(records)
comparisons := 0
edges := [][2]int{}
for _, blockRecords := range blocks {
c, e := compare(blockRecords)
comparisons += c
edges = append(edges, e...)
}
cc := connectedComponents(edges)
fmt.Printf("made %d comparisons and found %d matches and %d entities\n", comparisons, len(edges), len(cc))
for _, component := range cc {
names := make([]string, len(component))
for i, id := range component {
names[i] = records[id].Name
}
fmt.Printf("found the following entity: %s from %s\n", strings.Join(names, ", "), records[component[0]].City)
}
}
在 Go Playground 中尝试: go.dev/play/p/vP3tzlzJ2LN
连通组件函数遍历所有边,创建新的组件、将新 id 添加到现有组件中,或将两个组件合并为一个。结果看起来大致如下:
made 1052 comparisons and found 6 matches and 2 entities
found the following entity: Ivan Smxth, Ixan Smith, Ivax Smitx from Cairo
found the following entity: Brxan Williams, Brian Williams from Cape Town
保留这些边缘给我们带来了一些优势。我们可以利用它们使结果实体易于理解和解释,理想情况下,还能提供一个漂亮的用户界面,展示实体的记录是如何连接的。或者在使用实时实体解析系统时,我们可以利用这些边缘来拆分实体,特别是在数据被移除时。或者你可以在构建 图神经网络 (GNN) 时使用它们,从而获得比仅仅记录本身更好的机器学习结果。
实体的可视化表示(图像由作者提供)
边缘的一个问题可能会出现在有大量非常相似的记录时。例如,如果 A 与 B 匹配,B 与 C 匹配,那么 C 也可能与 A 匹配,具体取决于使用的规则。如果 D、E、F 等也与现有记录匹配,那么我们将回到平方增长的问题,很快会出现大量边缘,导致无法处理。
还记得我们如何构建阻塞桶吗?惊喜!对于非常相似的数据,最终都集中在几个巨大的桶中,计算性能会急剧下降——即使你遵循了之前创建多个属性桶的建议。
这种非相同重复项的典型例子是某人在同一家商店定期订购,但使用的是访客访问(抱歉,没有漂亮的客户 ID)。那个人可能几乎总是使用相同的送货地址,并且大多数情况下能够正确地写出自己的名字。因此,这些记录应该以特殊方式处理,以确保系统性能的稳定,但这是一个 单独的话题。
在你对获得的知识感到过于自满并想开始实施自己的解决方案之前,让我迅速打破你的幻想。我们还没有谈到实时处理这些问题的挑战。即使你认为不需要始终保持最新的实体(这是明显的好处),实时方法仍然提供了额外的价值:你不需要一遍又一遍地进行相同的计算,只需对新数据进行计算。另一方面,实施起来复杂得多。想做阻塞吗?将新记录与所属桶的所有记录进行比较,但这可能需要一段时间,并且可以被视为增量批处理。直到最终完成之前,还有大量新记录等待处理。想使用连通组件计算实体?当然,将整个图保存在内存中并只添加新的边。但是不要忘记跟踪由于新记录而刚刚合并在一起的两个实体。
所以,你仍然愿意自己实施这个方案。你做出了(在这种情况下)明智的决定,即不存储边缘并且不支持实时处理。于是你成功地运行了第一次实体解析批处理作业,处理了所有数据。这花了一段时间,但你每月只需执行一次,所以没问题。可能就在这个时候,你看到数据保护官从拐角处跑来,告诉你因 GDPR 投诉而需要从数据集中删除那个人。所以你为了一个被删除的实体重新运行了整个批处理作业——太棒了。
结论
进行实体解析乍一看可能相当简单,但它包含了许多重要的技术挑战。这些挑战中的一些可以被简化和/或忽略,但其他的则需要解决以获得良好的性能。
最初发表于 https://tilores.io。
时间序列分析中的移动平均综合指南
图片由 Midjourney 创建
探索简单移动平均和指数加权移动平均的细微差别
·发表于 Towards Data Science ·阅读时间 9 分钟·2023 年 10 月 30 日
--
移动平均是时间序列分析中的一种基本工具,广泛用于各种目的。在时间序列分析中,移动平均的一些最受欢迎的应用包括:
-
噪声减少: 移动平均有效地过滤掉时间序列数据中的随机波动和噪声。使用移动平均平滑数据有助于分析师关注潜在的模式,而不是短期波动,从而帮助他们识别有意义的趋势。
-
季节性分解: 将时间序列数据分解为其组成部分通常涉及使用移动平均。 季节性分解 技术利用移动平均从数据中提取季节性模式。此外,移动平均还可以帮助识别时间序列数据中的周期性成分,表示那些不完全是季节性的重复模式。
-
预测: 移动平均是各种预测模型的基本组成部分。
-
过滤异常值: 移动平均可以用于识别和过滤时间序列数据中的异常值。那些显著偏离移动平均的异常数据点可能被认为是异常值,并需进一步调查。
-
可视化平滑: 在绘制时间序列数据时,移动平均通常被用来创建更平滑、更清晰的可视化效果。这可以使向更广泛的受众传达趋势和模式变得更容易。
这些应用展示了移动平均在时间序列分析中的多样性。根据时间序列数据的具体目标和特征,我们可能会选择不同类型的移动平均和参数。虽然移动平均的概念看似简单,但可用的参数和选项可能会让人感到困惑。因此,在本文中,我们将详细探讨它们。
简单移动平均与指数加权移动平均
移动平均(MA)和指数加权移动平均(EWMA)是最受欢迎的两种移动平均类型。它们的关键区别在于对数据点分配权重的方式。
MA 计算固定数量最近数据点的平均值,赋予它们相等的权重。这意味着窗口中的所有数据点对移动平均数有相同的影响,较旧的数据点与最新的数据点同样重要。
另一方面,EWMA 是一种加权移动平均,它对随着时间推移变得越来越远的数据点赋予指数递减的权重。因此,较新的数据点接收更高的权重,使其在计算移动平均时更具影响力。这种方法使 EWMA 对数据的近期变化更加敏感,特别适合于跟踪短期趋势和快速响应时间序列中的变化。
设置和数据
我们从导入库开始。
接下来,我们生成具有以下特征的合成时间序列数据:
-
一年期的每日观察数据
-
时间序列中存在重复模式(季节性)
-
缺乏任何潜在的趋势
-
在 6 月 18 日至 6 月 23 日之间出现的显著峰值
-
最终步骤引入随机噪声
简单移动平均
在使用简单移动平均时,我们通常关注两个参数:窗口的大小以及窗口是否居中。
居中和无中心移动平均的区别在于移动平均相对于其计算的数据点的位置。
在中心移动平均中,平均值是对以感兴趣的数据点为中心的数据点窗口进行计算的。通常,窗口中使用的是奇数个数据点。例如,使用中心 3 天移动平均时,平均值是基于当前天和前一天及后一天计算的。
在无中心移动平均中,平均值是对向前移动的时间窗口的数据点进行计算,但窗口并不以感兴趣的数据点为中心。例如,要计算第t天的 3 天无中心移动平均,我们会取第t-2天、第t-1天和第t天的平均值。
在以下代码片段中,我们使用中心和无中心方法计算 7 天移动平均。
我们可以清楚地看到这两种方法之间的差异,特别是在 6 月的峰值周围。
对于中心化的 MA,平滑曲线中的峰值和谷值与数据中对应峰值和谷值的中点更为一致。
未中心化的 MA 滞后于数据点,这意味着它跟随数据的一般趋势,但对短期波动的反应不如快速。因此,移动平均曲线中的峰值和谷值在时间上相对于数据中的峰值和谷值有所偏移。
在接下来的片段中,我们生成了使用不同窗口大小的移动平均。从下图中可以观察到的一般模式是,窗口大小越大,移动平均对短期波动的敏感性越低。窗口大小较大的曲线更加平滑,并倾向于过滤短期噪声,突显出长期趋势。
最后需要记住的是,默认情况下,我们需要 X 个观测值来创建 X 期的移动平均。这就是为什么 180 天的移动平均仅在年中开始的原因。我们可以通过使用rolling
方法中的min_periods
参数来调整这一行为。
指数加权移动平均
如前所述,EWMA 为数据点分配指数递减的权重,给予近期观测值更多的权重,对较早的观测值则较少。这使得 EWMA 相比于简单移动平均对数据中的近期变化更为敏感。
使用平滑参数 alpha 的指数加权移动平均(EWMA)的公式通常以以下递归表示形式表达:
其中 x_t 是时间t的值,α是平滑因子,它控制在移动平均中最近观测值的权重。
在我们深入探讨选择平滑参数的各种方法之前,让我们澄清一个常见的混淆来源:调整公式与未调整公式。这一选择由ewm
方法中的adjust
参数决定。以下是供您参考的两种公式。
调整的目的是通过在初始时期除以衰减平滑因子来解决相对权重的不平衡问题。
数学细节: 在调整公式中,分母表示几何级数。因此,假设时间序列无限长,两种公式将得出相同的结果,即未调整值。*
为了说明实际差异,我们使用相同的平滑参数创建了两个 EWMA 线。区别在于公式中应用的调整。此外,我们将重点放在第一个月,因为那里的差异最为明显,尽管非常微小。
作为下一步,让我们深入探讨 alpha 值与计算移动平均时权重衰减速度之间的关系。为了计算随后的权重,我们使用源自调整版 EWMA 的公式。
在图表中,我们观察到较小的 alpha 导致权重衰减更慢。当 alpha 取其极端值 0.99 时,EWMA 实质上依赖于最新的数据点而忽略较旧的数据点。
接下来,让我们使用三个不同的 alpha 值:0.1、0.4 和 0.7 来可视化 EWMA 曲线。
我们可以观察到先前识别的模式在实际应用中的表现:alpha 越高,曲线对最近数据点的响应就越强。这种行为发生是因为更高的 alpha 值对近期观察值赋予了更大的权重,导致曲线紧随数据波动。相反,较低的 alpha 值对历史观察值赋予更多权重,结果是曲线变得更平滑,但滞后于数据的波动。
在使用指数加权移动平均时,选择负责指数衰减的参数会增加复杂性。虽然我们已经讨论了 alpha 参数,但它并不是唯一的选项。下面,我们描述了剩余的三个参数,并提供了选择正确值的一些直觉:
-
Half-life (
halflife
) — Half-life 实质上是衰减量减少到其初始值一半所需的时间。较小的 half-life 使最近的观察更具影响力,从而使 EWMA 对短期变化更为敏感。相反,较大的 half-life 对较旧的数据赋予更多的权重,导致 EWMA 更平滑且变化较慢。 -
Span (
span
) — Span 表示有效的窗口大小或在 EWMA 计算中考虑的周期数。换句话说,它表示有多少过去的观察值对平滑过程有贡献。较大的 span 包含更多的历史数据,导致 EWMA 更平滑且变化较慢。相反,较小的 span 更关注最近的数据,使其对短期变化更为敏感。 -
Center of Mass (
com
) — 类似于 span,center of mass 参数定义了 span 内的相对位置。它指示了加权函数质量的主要分布位置。center of mass 为 0.5 相当于 span 的中点,给予两侧观察值相等的权重。调整 center of mass 允许我们将加权偏向于近期或较旧的数据点。较小的com
将质量中心移向当前,强调近期数据并增加对短期波动的敏感性。另一方面,较大的com
将质量中心移向过去,导致 EWMA 更平滑且变化较慢。
以下公式展示了这三种替代参数与平滑因子 alpha 之间的关系。
为了更好地理解这个概念,让我们考虑一个处理季度数据的场景。在我们的指数加权移动平均中,我们希望纳入过去一年的数据。为此,我们选择了一个对应 alpha 值为 0.4 的跨度 4。下表展示了一些示例值。
最后,让我们看看半衰期参数。我们已经提到,它对应于一个量被减少到其初始值一半的时间周期数。考虑一个半衰期为 10 的情况,这对应于 alpha = 0.066967。如果我们使用该 alpha 计算以下权重,我们可以清楚地看到第 10 次观测的权重等于 0.5,正如我们预期的那样。
总结
-
移动平均在时间序列分析中有多种用途,如噪声减少、季节性分解、预测、异常值过滤和创建更平滑的可视化。
-
简单移动平均将远期和近期的观测值赋予相等的权重。
-
相比之下,指数加权移动平均通过赋予近期数据更高的权重来更快速地响应最新数据。我们可以通过以下参数之一来微调平滑程度:平滑因子(alpha)、半衰期、跨度或质心。
你可以在这里找到本文中使用的代码。像往常一样,任何建设性的反馈都非常欢迎。你可以通过LinkedIn、Twitter 或评论联系我。
喜欢这篇文章?成为 Medium 会员,继续通过阅读无限制地学习。如果你使用 这个链接 成为会员,你将支持我而不会额外增加你的成本。提前感谢,期待再见!
你也可能对以下内容感兴趣:
了解如何通过使线性模型对趋势变化更灵活来提高其拟合度
towardsdatascience.com ## 解锁线性回归中交互项的威力
了解如何通过包含交互项使你的线性模型更加灵活
towardsdatascience.com ## 指标层:所有 KPI 定义的单一真实来源
了解为什么实施指标层将使在组织中获取数据驱动的洞察更加稳健!
towardsdatascience.com
进一步阅读
所有图片,除非另有说明,均为作者(以及可能的一些人工智能)提供。
控制渐变
为什么正分割可能对你的下一场马拉松最为合适。
·发表于 Towards Data Science ·13 min 阅读·2023 年 9 月 3 日
--
图片由 Miguel A Amutio 提供,来源于 Unsplash
休闲跑者是否应该遵循传统智慧,即负分割(后半程更快)是跑马拉松的最佳方式?
关键结果
-
马拉松跑者通常建议在比赛中保持节奏,以便后半程(稍微)快于前半程——这是一个负分割——因为 “这是精英获胜的方式” 以及如何打破记录。然而,这项分析基于超过 400 万场马拉松 表明,几乎 90%的比赛后半程完成时间较慢(即正分割)。
-
当我们控制跑者能力时,节奏更加均匀的比赛与更快的完成时间和更多的个人最佳相关。然而,最佳的休闲表现与略微的正分割相关,表明 控制渐变 是一种有效的休闲跑者节奏策略。
-
证据还指出,休闲跑者对于负分割存在表现偏见——例如,10%的正分割与更好的结果(更多的个人最佳、改善的相对完成时间)相关,而不是 10%的负分割——因为休闲跑者通常对负分割的节奏把控过于保守。
-
这项分析建议休闲跑者应该将 1.5%的正分割作为其A 目标(即,目标是后半程比前半程长 1.5%),并将正分割低于约 3%作为B 目标。
介绍
传统智慧建议马拉松比赛应以均匀或稍微负差距进行,并且认为“把时间存入银行”通过更快的前半程(即正差距)不是一个好主意。支持这一观点的证据包括常被引用的统计数据,即大多数 800 米以上的世界纪录都是通过负差距实现的,但我们是否应该期望精英的成功能很好地转移到休闲跑者身上?毕竟,精英马拉松选手在管理他们的配速、努力和能量方面有更多经验,而大多数休闲跑者可能只完成了几次马拉松全程。
在撰写时,一项《Runner’s World》调查(见下文)显示,仅有 11%的跑者在比赛中瞄准负差距。那么,休闲跑者是否正确地放弃传统智慧,选择正差距?或者我们是否应该采取更有纪律的方法来规划我们的配速?
《Runners World》的一项调查结果显示,跑者通常是否计划以负差距或正差距完成比赛。
这是最近一篇《Runner’s World》的文章讨论负差距智慧的问题,并对故意的正差距,即所谓的受控减速进行了推测,其中跑者在比赛的后半程比前半程慢 30 秒至 3 分钟。例如,在受控减速中,一位 4 小时(240 分钟)的马拉松选手可能在 119 分钟内完成前半程,在 121 分钟内完成后半程,从而得到约+0.01 的相对半程分差(RHS)——1%的正差距——基于以下公式。相反,另一位跑者可能以 121 分钟的前半程和 119 分钟的后半程达到相同结果,得到-0.01 的负差距。
接下来,我们将更详细地探讨这个话题,使用一个来自 2005 至 2019 年间全球大城市马拉松的公开比赛记录的大型数据集。该数据集包含超过 400 万条独特的比赛记录(包括 5 公里/~3 英里分裂时间和整体完成时间),涉及约 270 万名独特的跑者。这些比赛记录来自 38 个城市的 270 场不同马拉松。数据集的完整细节,包括可以访问数据的马拉松网址,已在其他地方提供。
负分裂和正分裂有多常见?
当然,跑者声称偏好正分裂并不意味着他们的比赛就是这样进行的。然而,在我们的数据集中,我们发现 89%的比赛确实是通过正分裂完成的——这与 Runner's World 的调查结果非常接近——且男性和女性之间没有显著差异。值得注意的是,虽然负分裂的平均 RHS 在男性和女性之间非常相似(分别为-0.045 和-0.044),但男性的正分裂平均 RHS(0.16)大于女性(0.13),且男性的 RHS 变异性要大得多。
图 1(a)显示了具有各种 RHS 值的比赛比例。最常见的 RHS 约为 0.05(比赛的第二半程比第一半程长 5%)。对于不同程度的负分裂(RHS<0),男性和女性跑者的比例几乎相同,但在跑正分裂的男性和女性比例上存在一些差异。女性跑者比男性更有可能跑出适中的正分裂(0<RHS<0.2),而男性更有可能跑出极端的正分裂(RHS > 0.2);这与之前的研究一致,研究表明男性比女性更容易出现撞墙现象(即非常大的正分裂)。当我们根据比赛的每半程之间的分裂差异(即第二半程时间 - 第一半程时间)比较跑者时,在(b)中我们看到类似的模式,尽管这一衡量标准下男性和女性跑者之间的差距不那么明显。
图 1. 比较不同相对半程分裂和分裂差异(分钟)的比赛和完成时间的比例。
性能影响
分裂类型和程度如何影响完成时间?在图 1(c)和(d)中,我们根据相对分裂度(RHS)和分裂差异(分钟)分别绘制了男性和女性跑者的平均完成时间。当分裂变得不那么均匀(负向和正向)时,会有完成时间的成本,最快的完成时间与男性和女性跑者的略微正分裂相关。
还要注意的是,图 1(c)和(d)中的完成时间并不对称。负分配的时间似乎比正分配的对应时间增加得更快。我们可以在图 2(a)和(b)中更清楚地看到这一点。我们将男性和女性跑者的平均完成时间与其绝对RHS 进行对比,实际上是对负分配时间进行镜像处理,以便更直接地与正分配时间进行比较。在每种情况下,对于给定的绝对 RHS,正分配的完成时间比对应的负分配完成时间要快;例如,5%的正分配比 5%的负分配要快。而且,对于逐渐增加的绝对 RHS 值,正负完成时间之间的差异(阴影区域)也在增加,男性的增加幅度大于女性。
图 2. 比较男性和女性的负分配和正分配的平均完成时间。
控制能力
上述分析的一个缺陷是没有考虑到较快的跑者往往进行更加均匀配速的比赛。因此,较快的跑者占据了更均匀 RHS 组的主要部分,从而导致更快的完成时间。解决这个问题的一种方法是寻找一种独立于跑者能力的性能评估方式。
为此,我们关注数据集中至少完成过 5 场马拉松的跑者——共有 101,120 名跑者,他们共完成了 680,921 场比赛——对于每位跑者,我们确定其最快完成时间作为其个人最佳(PB)时间。当然,这不能保证是他们真实的 PB,因为这些跑者中至少有一些会在我们的数据集之外完成马拉松。然而,这将作为他们真实 PB 时间的一个有用(保守)估计,并且我们可以用它来评估跑者相对于其 PB 的表现;我们定义非 PB 比赛的相对完成时间为其完成时间除以跑者的 PB 时间,因此相对完成时间为 1.1 意味着这场比赛比跑者的 PB 慢 10%。
超过 80%的跑者在个人最佳成绩(PB)中完成的成绩与他们非 PB 比赛的更均匀分配(RHS ~ 0.069)相比要好于平均分配(RHS ~ 0.13),这进一步支持了更均匀配速是一种良好策略的假设。在下图 3(a)中,我们可以看到这些相对完成时间也随着配速变得更加均匀而得到改善,而在图 3(b)中,这些更均匀配速的比赛中,PB 的比例也显著更高。
图 3. 比较男性和女性跑者基于配速的表现,使用(a)完成时间作为个人最佳时间的分数和(b)PB 比赛的比例。
因此,尽管更快的跑者的比赛配速更均匀,我们仍然发现跑者的最佳(PB)比赛比非 PB 比赛更均匀,无论能力如何。此外,非 PB 比赛的相对表现随着配速的均匀性提高而改善,并且,无论能力如何,最多 PB 和最佳相对完成时间都与略微的正向分裂相关。
再次在上面的图 3(a) 中,我们可以看到相对完成时间在正向分裂的比赛中有偏向。图 4(a) 和 (b) 展示了这一点,再次通过根据绝对 RHS 值绘制跑者的相对完成时间,分为男性和女性。在这两种情况下,正向分裂的相对完成时间优于负向分裂。
图 4. 比较男性和女性在负向和正向分裂下的相对完成时间。
为什么会有偶数和正向分裂偏差?
尽管更均匀的配速与更好的表现一致,但似乎是正向分裂,而非负向分裂,对休闲跑者更有益。这是为什么呢?我们的工作假设是,这归结于跑者是否能够以这样的方式控制比赛配速,以至于他们在最后阶段投入了最大的努力,刚好在储备完全耗尽时完成比赛。
虽然这听起来很明显,但并不是显而易见的结论。跑者可能会以均匀的配速完成比赛,但没有过度推动自己——放弃 PB——以在当前能力范围内完成比赛。或者跑者可能会跑出过于保守的负向分裂,即使第二阶段更快,他们在终点线前还有更多体力;这又是一个错失 PB 的机会。或者,像往常一样,计划中的正向分裂,如果第一阶段过快,会导致马拉松选手在终点线前很久就已经体力耗尽,并且减速过多,无法 PB。
我们认为,适度 正向分裂(0 < RHS < 0.25)似乎比其对应的 适度 负向分裂(-0.25 < RHS < 0)更好,至少在休闲跑者中,这是因为适度的正向分裂比适度的负向分裂更容易“做到正确”。换句话说,跑者进行适度的负向分裂似乎面临着更大的风险,即在比赛结束时还有剩余体力,导致表现不佳,而进行相应正向分裂的跑者则面临在结束前体力耗尽的风险。
我们可以通过观察跑者完成马拉松最后阶段(我们数据集中最后的 2.2 公里或约 1.37 英里)的情况来获得一些见解,因为他们在比赛这一部分的减速或加速程度将告诉我们很多关于他们当前状况的信息。在图 5(a)中,我们绘制了不同 RHS 范围内最后 2.2 公里的相对配速(实际最后配速除以平均比赛配速)。跑负分段的人通常会加速(相对最终配速<1),而跑正分段的人则趋向于减速(相对最终配速>1)。
重要的是,范围在-0.2<RHS<0 的负分段者的平均相对最终配速为 0.89(对应 11%的加速),而正分段(0<RHS<0.2)的平均相对配速仅为 1.07(7%的减速)。换句话说,在马拉松的最后阶段,适度的负分段者往往比对应的正分段者加速更多,而正分段者几乎可以肯定会感到疲惫(他们在终点临近时甚至无法加速),而负分段者可能不会出现这种情况。这种表现偏差与这种可能性一致。
图 5. 根据跑步者的分段成绩,马拉松最后阶段(2.2 公里/约 1.37 英里)的平均加速速度和加速跑者的比例。
在图 5(a)中,男女线条在 0.005 < RHS < 0.03 之间交叉于平均比赛配速线(相对最终配速=1),这表明这是一个配速甜点区,因为在这个狭窄的 RHS 范围内,跑者以没有剩余的状态到达终点——他们能够保持配速,但无法加速也不会减速——因此我们在本分析中早些时候看到了这个 RHS 范围内的最佳表现。换句话说,轻微的正分段在这个 RHS 范围内与更快的完成时间和更多的 PB 相关,因为它使跑者以没有剩余的状态到达终点。拥有相同或更负的分段跑者有足够的储备来加速,而更正的分段跑者(RHS>0.03)则过早减速,并在越过终点线之前耗尽了储备。
当然,这并不是说小的正配速没有风险。可以推测,许多以大正配速(RHS>0.2)完成马拉松的跑者在开始时心中设想的是较为温和的配速,但也许他们对早期的配速或比赛日的补给判断不准确。负配速的相应危险在于过于保守地配速早期阶段。图 5(b)显示了在马拉松最后 2.2 公里中加速(即使只是稍微加速)的跑者比例,我们看到女性跑者无论配速如何更可能会加速。例如,在那些跑 4%正配速(RHS=0.04)的跑者中,大约 45%的女性跑者在比赛结束时加速,而男性跑者只有 25%。
如果在比赛末尾加速等同于早期保守的配速,那么这可能表明女性跑者的配速比男性跑者更为保守,即使是在那些配速大致均匀的跑者中也是如此。男性配速更激进的想法并不新鲜,但通常被归因于极端的正配速实例,例如那些撞墙的情况。这里我们看到的证据表明女性跑者在整个 RHS 值范围内配速较为保守。在图 3(a)中也可以明显看出,男性在广泛的 RHS 范围内往往表现略好(相对于他们的 PB),这与女性跑者更为保守的配速观察一致。也许女性跑者可以通过调整配速稍微减少保守度来提高表现?
受控渐退
我们所呈现的略微正配速的证据与所谓的受控渐退配速模式非常接近。通常表现为适度(30 秒—3 分钟)的正配速,过去曾推荐这种模式,因为业余跑者可能会从听到他们不必跑负配速以实现个人最好成绩的正心态转变中受益。
这项分析进一步提供了支持受控渐退的具体证据。例如,图 6(a)放大了这一受控渐退区域的比赛相对完成时间,我们可以看到男性和女性的最佳表现与这一区域相关。
图 6. 与受控渐退相关的比赛相对完成时间。
图 6(b)使用相对半程配速而非分钟配速差异来突出相应区域。我们可以看到类似的结果,最佳相对完成时间对应于 0.005 到 0.03 之间的相对半程配速(0.5%—3%的正配速)。
在每种情况下,最佳的完成时间都与正向分裂相关,其中第二半程时间比第一半程时间长约 90 秒,或相对半程分裂约为 0.015。因此,可以认为,跑者的 A 目标可能是瞄准略微正向的分裂(90 秒或约 1.5%),B 目标则是将正向分裂保持在 3 分钟以下(或约 3%)。在这些分裂之外(无论是负分裂还是正分裂),表现往往会下降。
结论
本文开始时质疑了传统观念,即休闲跑者应计划在下一次马拉松中实现负分裂,主要因为精英跑者似乎都这样做。我们发现这种建议很少被遵循,也许有充分的理由,因为略微正向的分裂(0.5-3%)似乎为休闲跑者提供了一个理想的配速点,而最佳表现通常出现在第二半程比第一半程长 1.5%的比赛中。这与控制衰退的配速模式非常接近。
这一分析的重要局限性在于,虽然我们的数据集反映了比赛期间发生的情况,但它并未告诉我们跑者是否按计划或预期的方式进行比赛。如前所述,一些计划进行均衡或适度正向分裂的跑者可能最终会出现更大的正向分裂。这就是马拉松的本质。即使有最好的意图,一些负分裂也可能转变为正分裂,具体取决于训练、补给和比赛条件等因素。
当然,对于许多休闲跑者来说,马拉松并不只是为了打破个人最佳记录。马拉松的乐趣来自于完成比赛的“快乐”而非打破个人最佳的痛苦。尽管如此,数据表明,略微正向的分裂是一种明智的策略,无论你的比赛目标如何!
特别感谢 Amby Burfoot 和 Padraig Cunningham 对本文早期草稿的宝贵反馈。
除非另有说明,本文中的所有图片均由作者创作。
概率关系的反直观性质
如果 y 可以被估计为 x 的线性函数,并不意味着 x 也可以被估计为 y 的线性函数
·
关注 发表在 Towards Data Science · 10 分钟阅读 · 2023 年 7 月 21 日
--
一个概率关系的示例(与图 1A-B 中的可视化风格相同)—— 作者提供的图像
考虑两个实值变量 x 和 y,例如,父亲的身高和儿子的身高。统计学中回归分析的核心问题是通过已知 x 来预测 y,例如,根据父亲的身高预测儿子的身高¹。
线性回归的想法是使用x的线性函数作为对y的猜测。正式地,这意味着将ŷ(x) = α₁x + α₀作为我们的猜测,并通过最小化y和ŷ之间的均方误差来找到α₀和α₁。现在,假设我们使用一个巨大的数据集并找到α₀和α₁的最佳值,这样我们就知道了如何根据x找到y的最佳估计。那么我们如何利用这些最佳的α₀和α₁来根据y找到关于x的猜测x̂(y)呢?例如,如果我们总是能根据父亲的身高来猜测儿子的身高,那么我们可以根据儿子的身高猜测父亲的身高吗?
这些问题是“我们如何使用ŷ(x)来找出x̂(y)?”的特殊情况。虽然这听起来可能很简单,但这个问题实际上非常难以解决。在本文中,我研究了ŷ(x)和x̂(y)在确定性和概率性环境下的联系,并表明我们对ŷ(x)和x̂(y)在确定性环境下的关系的直觉不能推广到概率性环境中。
问题的正式表述
确定性环境
所谓确定性环境,我指的是(i)没有随机性和(ii)每个x的值总是对应于相同的y的值。正式地,在这些环境中,我写作y = f(x),其中f: R → R是某个函数。在这种情况下,x以完全确定的方式决定y(即,没有随机性或噪声),ŷ(x)的最佳选择是f(x)本身。例如,如果一个儿子的身高总是其父亲身高的 1.05 倍(暂时忽略这个例子的不可行性!),那么我们对儿子身高的最佳猜测就是将父亲的身高乘以 1.05。
如果f是一个可逆函数,那么x̂(y)的最佳选择等于f的逆。在上面的例子中,这意味着对父亲身高的最佳猜测总是儿子身高除以 1.05。因此,确定性情况下ŷ(x)和x̂(y)之间的联系是直接的,可以简化为找到函数f及其逆。
概率性环境
在概率性环境中,x和y是随机变量X和Y的样本。在这种情况下,单个x的值可以对应多个y的值,为了最小化均方误差,ŷ(x)的最佳选择是条件期望E[Y|X=x]—见脚注²。用更容易理解的话来说,这意味着如果你训练一个非常有表现力的神经网络来预测给定x的y(使用一个足够大的数据集),那么你的网络将会收敛到E[Y|X=x]。
类似地,x̂(y) 的最佳选择是 E[X|Y=y] — 如果你训练你的表达能力强的网络来预测 x 给定 y,那么它原则上会收敛到 E[X|Y=y]。因此,在概率设置中,ŷ(x) 如何与 x̂(y) 相关的问题可以重新表述为条件期望 E[Y|X=x] 和 E[X|Y=y] 之间的关系。
本文的目标
为了简化问题,我专注于线性关系,即 ŷ(x) 在 x 上是线性的。线性确定性关系具有线性逆,即 y = αx(对于某个 α≠0)意味着 x = βy 且 β = 1/α — 见脚注³。与确定性关系 y = αx 类似的概率线性关系为
公式 1
其中 Z 是一个附加的随机变量,通常称为“噪声”或“误差项”,其条件平均值假定为零,即 E[Z|X=x] = 0 对所有 x 都成立;注意,我们并不总是假设 Z 与 X 是独立的。利用公式 1,给定 X=x 的 Y 的条件期望为(见脚注⁴)
公式 2
公式 2 说明条件期望 ŷ(x) 在 x 上是线性的,因此可以看作是线性确定性关系 y = αx 的概率对应。
在本文剩余部分,我将提出两个问题:
-
公式 2 是否意味着 x̂(y) := E[X|Y=y] = βy 对某个 β≠0?换句话说,公式 2 中的线性关系是否有线性逆?
-
如果确实存在 x̂(y) = βy,那么我们可以像在确定性情况下那样写 β = 1/α 吗?
我使用了两个反例,表明尽管听起来有些反直觉,但两个问题的答案都是否定的!
示例 1:当 β 不是 α 的逆时
作为第一个例子,考虑线性回归问题的最典型设置,总结为以下三个假设(除了公式 1;见图 1A进行可视化):
-
误差项 Z 与 X 独立。
-
X 服从均值为零、方差为 1 的高斯分布。
-
Z 服从均值为零、方差为 σ² 的高斯分布。
图 1. 可视化示例 1 和示例 2。 面板 A 和 B 可视化了示例 1 (A;α = 0.5,固定 σ² = 3/4) 和示例 2 (B;α = 0.5,σ² 依赖于 x) 的 Y 在给定 X 条件下的分布。给定随机变量 X 的一个值,随机变量 Y 在这两个示例中都服从高斯分布:黑色线条显示条件期望 E[Y|X=x],阴影区域显示高斯分布的标准差。点显示了 (X, Y) 的 500 个联合分布样本。面板 C 显示了 Y 的边际分布(X 服从标准正态分布),示例 1 (蓝色) 和示例 2 (红色):示例 1 中 Y 的边际分布是均值为零、方差为 α² + σ² 的高斯分布,但我们只能数值评估示例 2 中 Y 的边际分布。
经过几行代数计算,很容易证明,这些假设意味着 Y 服从均值为零、方差为 α² + σ² 的高斯分布。此外,这些假设还意味着 X 和 Y 是联合高斯分布,均值为零,协方差矩阵等于
由于我们拥有 X 和 Y 的完整联合分布,我们可以推导它们的条件期望(见脚注⁵):
因此,给定我们第一个示例的假设,方程 2 具有形式为 x̂(y) = βy 的线性逆,但 β 不等于其确定性对应的 1/α ——除非我们有 σ = 0,这 等同于 确定性情况!
这一结果表明,我们对确定性线性关系的直觉不能推广到概率线性关系。为了更清楚地看到这一结果所暗示的真正疯狂程度,让我们首先考虑 α = 0.5 的确定性情况 (σ = 0;图 2A 和 2B 中的蓝色曲线):
这意味着,给定一个 x 值,y 的值是 x 的一半;给定一个 y 值,x 的值是 y 的两倍,这看起来是直观的。重要的是,我们总是有 x < y。现在,让我们再次考虑 α = 0.5,但这次 σ² = 3/4(图 2A 和 2B 中的红色曲线)。这种噪声方差的选择意味着 β = α = 0.5,结果是
这意味着,给定一个 x 值,我们对 y 的估计是 x 的一半;然而,给定一个 y 值,我们对 x 的估计也是 y 的一半!奇怪的是,我们总是有 x̂(y) < y 和 ŷ(x) < x ——如果变量是确定性的,这将是不可能的。看似反直觉的是,方程 1 可以重写为
方程 3
然而,这只能意味着(与 方程 2 相对)
方程 4
问题在于,虽然我们设计时有E[Z|X=x]=0,但我们不能对E[Z|Y=y]及其对y的依赖性做出任何说明!换句话说,使得x̂(y)与y/α不同的是观察值y还包含关于误差Z的信息,例如,如果我们观察到一个非常大的y值,则意味着很可能误差Z也具有一个较大的值,这应在估计X时考虑进去。
图 2. 示例 1 和示例 2 中的线性关系及其逆关系。 面板A展示了在示例 1 和示例 2 的概率设置下ŷ(x)与 x 之间的线性关系(红色;α = 0.5)和 y 与 x 之间的等效确定性关系(蓝色);注意ŷ(x)作为函数 x 在两个示例中是相同的。面板B和C展示了在示例 1(面板B中的红色;固定σ² = 3/4)和示例 2(面板C中的红色;σ² 依赖于 x)的概率设置下x̂(y)与 y 之间的逆关系。蓝线显示了参考的等效确定性关系的逆关系。在所有面板中,虚线黑色显示了 y=x 线。
这是对看似矛盾的陈述的简单解释,例如“高个子父亲的儿子(平均而言)也很高,但没有父亲那么高,同时,高个子的儿子有(平均而言)高个子父亲,但没有儿子那么高”!
总结,我们的示例 1 显示,即使概率线性关系ŷ(x) = αx具有形式为x̂(y) = βy的线性逆关系,斜率β也不一定等于其确定性对等物1/α。
示例 2:当x̂(y) 是 非线性时
只有当方程 4中的E[Z|Y=y]也是y的线性函数时,形式为x̂(y) = βy的逆关系才是可能的。在第二个示例中,我对示例 1 进行了一些小修改,以打破这一条件!
特别地,我假设误差项Z的方差依赖于随机变量X——与示例 1 中的假设 1 相对。正式地,我假设(除了方程 1;有关可视化见图 1B):
-
X服从均值为零、方差为 1 的高斯分布(与示例 1 中的假设 2 相同)。
-
给定X=x,误差Z服从均值为零、方差为σ² = 0.01 + 1/(1 + 2x²)的高斯分布。
这些假设实际上意味着,给定X=x,随机变量Y服从均值为αx、方差为0.01 + 1/(1 + 2x²) 的高斯分布(见图 1B)。与示例 1 中X和Y的联合分布是高斯分布不同,示例 2 中X和Y的联合分布没有优雅的形式(见图 1C)。然而,我们仍然可以使用贝叶斯规则,找到给定Y=y的X=x的相对复杂的条件密度(见图 3获取一些数值评估的示例):
方程 5
其中曲线 N 表示高斯分布的概率密度。
图 3 . 示例 2 中给定 Y=y 的 X 的条件分布。先验分布 p(x)(蓝色曲线)、似然 p(y|x)(橙色曲线)和后验分布 p(x|y)(黑色曲线;使用 方程 5 进行数值评估),从左到右,y = 0.5、1.5 和 2(假设所有情况下 α = 0.5)。
我们可以使用数值方法来评估条件期望
方程 6
对于给定的 y 和 α,图 2C 显示了 x̂(y) 随 y 变化的函数,当 α = 0.5 时。尽管听起来可能违反直觉,但这种反向关系是高度非线性的——这是由于 图 1B 中显示的 x 依赖的误差方差所致。这表明,即使 y 可以很好地作为 x 的线性函数进行估计,并不意味着 x 也可以很好地作为 y 的线性函数进行估计。这是因为 方程 4 中的 E[Z|Y=y] 在我们超出类似于示例 1 的标准假设时,可能会对 y 产生任何奇怪的函数依赖。
总结,我们的示例 2 显示了概率线性关系 ŷ(x) = αx 并不一定具有 x̂(y) = βy 的线性逆关系。重要的是,x̂(y) 和 y 之间的反向关系依赖于误差项 Z 的特性。
结论
在我们的教育过程中,我们中的大多数人对确定性关系建立了丰富的直觉——基于我们在微积分、分析等领域看到的所有有趣结果。然而,至关重要的是要认识到这种直觉的局限性,并且在考虑概率关系时不能信任这种直觉。特别是,示例 1 和 2 表明,即使是极其简单的概率关系也可能违背我们的直觉。
致谢
我感谢 Johanni Brea、Mohammad Tinati、Martin Barry、Guillaume Bellec、Flavio Martinelli 和 Ariane Delrocq 对本文内容的有益讨论和宝贵反馈。
代码:
所有用于分析的代码(使用 Julia 语言)可以在 这里 找到。
脚注:
¹ 有兴趣的读者可以在“父亲的身高如何影响儿子的身高”中查看这个问题的易懂处理。
² 详见维基百科上的 “最小均方误差” 页面。
在不失一般性的情况下,我们总是假设 x 和 y 的平均值为零。因此,在父亲和儿子的身高示例中,x 和 y 表示它们的身高与父亲和儿子的平均身高之间的差异。
方程 1 和 方程 2 之间的关系是可逆的,即,如果 方程 2 是 X 和 Y 的唯一约束,那么我们总是可以将 Y 表示为 方程 1 中的形式,且存在一个满足 E[Z|X=x] = 0 的随机变量 Z。
请参阅维基百科上的 ‘多元正态分布’ 页中的“二元条件期望”部分。
文字数据的创造性、偶尔混乱的世界
·
跟踪 发表在 Towards Data Science · 作为 Newsletter · 3 分钟阅读 · 2023 年 11 月 16 日
--
几年来,文本与数据的交集(多多少少)仅限于自然语言处理(NLP)领域——利用文本数据进行预测、分类和推荐工具的广泛机器学习任务。
大型语言模型的崛起为该领域引入了一系列令人兴奋的新可能性,各种新用例和创新工作流程如雨后春笋般涌现。本周的亮点代表了对这一新兴领域深入挖掘的一系列概念和方法。从提示工程到文本转图像和文本转语音应用,我们很高兴分享探索这些强大模型的文本数据作为输入和输出的创造性可能性的作者的工作。让我们深入了解。
-
迷失在 DALL-E 3 翻译中当你在非英语语言中使用 DALL-E 3 等文本转图像工具时会发生什么?Yennie Jun继续探索在使用资源匮乏语言的用户中,模型性能存在的差异以及性别和其他偏见如何渗透到生成的图像中。
-
如何将任何文本转换为概念图在他的最新文章中,Rahul Nayak深入探讨了知识图增强生成的世界,引导我们通过将文本语料库转化为概念图(GC),然后可视化来检测模式并得出有意义的洞见。
-
RAG:如何与您的数据交谈在过去几个月中,我们多次涉及检索增强生成技术,但Mariya Mansurova对这一话题的贡献仍然非常值得您花时间阅读:她提出了一个引人入胜的实用工作流程,用于分析使用 ChatGPT 的客户反馈。
-
FastSpeech:论文概述与实现近年来,文本转语音工具取得了重大进展。要深入了解它们的工作原理以及如何利用变压器来提高它们的性能,请不要错过Essam Wisam对 2019 年 FastSpeech 论文的易于理解的介绍,这为我们在该领域看到的许多进展提供了便利。
-
释放文本数据的力量与 LLMs如果你是一个初学者,想要开始尝试尖端的文本数据技术,索非亚·罗莎的分步指南将帮助你迅速入门。它将带领我们完成从下载数据到使用 GPT-3 和分析结果的整个工作流程。
-
通用提示工程路线图:情境支架框架(CSF)提示工程已成为人类直觉与大型语言模型能力之间互动中的关键组成部分。朱塞佩·斯卡拉莫尼亚超越了基本的提示技巧,介绍了情境支架框架(CSF),这是一个“有效提示工程的通用心理模型”。
我们希望你这周能有时间涉猎其他主题——以下是我们最近在数据可视化、生成内容检测等方面的一些突出成果:
-
人工智能能否帮助我们理解大脑的工作原理?斯蒂芬妮·申通过绘制生物学习与人工神经网络之间的相似性来探讨这个重要问题。
-
Matplotlib 是一个无处不在且强大的可视化工具,但也有其独特的怪癖。李·沃恩的初学者友好指南将帮助你踏上学习之旅的正确起点。
-
对于所有关注营销的数据科学家:不要错过竹田一关于客户生命周期价值预测的清晰且详细介绍。
-
区分人类生产的内容与模型生成的内容的能力从未如此重要——也从未如此困难。斯蒂芬妮·基尔默解读了当前围绕这一难题的风险和挑战。
-
本周想做些实际操作吗?Amanda Iglesias Moreno的教程将引导你构建六边形地图,使用 H3 和 Plotly。
-
在他最新的深度分析中,Jeffrey Näf详细探讨了随机森林中的变量重要性,涵盖了传统方法和较新的发展。
感谢你支持我们作者的工作!如果你喜欢你在 TDS 上阅读的文章,可以考虑成为 Medium 会员——这将解锁我们的整个档案(以及 Medium 上的所有其他帖子)。
直到下一个 Variable,
TDS 编辑团队
选择损失函数在创建准确的时间序列预测中的关键作用
通过机器学习掌握时间序列预测
你的损失函数选择如何影响时间序列预测的准确性
·发布在 Towards Data Science ·8 分钟阅读·2023 年 3 月 14 日
--
介绍
在这篇文章中,我将向你展示我认为在机器学习中常被忽视的一个重要方面,即损失函数的选择。我将通过介绍我在Driven Data主办的登革热竞赛中的方法来实现这一点。
我已经建立了一个岭回归模型作为基准模型,并使用不同损失函数构建了几种“风味”的 XGBoost 回归模型。
竞争者被要求预测伊基托斯和圣胡安每周时间间隔内的登革热总病例。每位竞争者根据其模型在测试数据集上的平均绝对误差(MAE)进行排名。要了解更多关于挑战、登革热或自己参与竞赛的信息,可以访问挑战的 主页。
笔记本与代码库
我已经通过下面的链接提供了我的工作 Jupyter 笔记本和 GitHub 代码库。笔记本可能需要几分钟才能加载,请耐心等待。
请随意在笔记本中尝试你自己的损失函数。
数据
数据以 csv 格式提供,包括训练特征、标签和测试特征(类似于 Kaggle 比赛格式)。数据特征包括天气预报、气候预报和植被指数。
标签(登革热总病例数)具有长尾特征,极端值较少。这在两个城市中都成立。这些分布的形状应该能给你一些线索,关于什么可能是适合模型的损失函数。
本项目使用的数据由 Driven Data 策划和提供,按照 Driven Data¹ 的规定可以在比赛之外免费使用。数据的原始来源可以从 国家海洋和大气管理局获得,数据公开且可以免费使用,具体根据 NOAA 的条款和条件。
作者提供的图片:登革热总病例数 Iquitos 的分布
作者提供的图片:时间序列 Iquitos
作者提供的图片:登革热总病例数 San Juan 的分布
作者提供的图片:时间序列 San Juan
数据预处理
除了拆分数据外,我进行了三个数据预处理步骤。第一步是填补缺失值。我简单地用这些缺失特征的均值进行填补,这是一种快速且粗略的方法,避免了过多的分布偏移。
第二步是对数据进行标准化。标准化是一种特征缩放的形式,将数值特征的方差设置为 1,均值设置为 0。虽然 XGBoost 不需要缩放,但在拟合 Ridge 回归模型之前缩放特征是有用的。
我应用的第三个操作是独热编码。这将类别变量转换为其数值表示,以便可以用来训练模型。
注意:为了防止数据泄漏,所有预处理步骤都在训练集、验证集和测试集上分别进行。
特征工程
我构建了一些滞后特征。这些特征对于提高时间序列模型的预测性能特别有用,因为它们能够利用自相关性。
数据中存在一些限制,这限制了我能够创建的滞后期。首先,测试数据比训练数据提前了 3 年,并且没有标签。这意味着我能够创建的最短滞后期是 3 年,这感觉相当长。接下来,病例数在周级别上记录得不均匀。我不得不通过在月级别上聚合病例数,并基于这些聚合创建滞后特征来克服这个问题。滞后特征被设计为均值、最小值和最大值。
机器学习方法
有两个独立的时间序列跟踪登革热的总病例数,按周记录。其中一个时间序列用于 Iquitos (Iq),另一个用于 San Juan (Sj),我分别为每个时间序列训练了模型。
这帮助我将机器学习概念化为一个优化问题,在这个问题中,你试图最小化一个目标函数。
目标函数 = 损失函数 + 正则化函数
损失函数决定了如何“评分”模型在预测标签方面的整体表现,在这个案例中就是登革热的总病例数。正则化函数惩罚模型复杂性,有助于减轻过拟合。
数据拆分
在深入讨论超参数调整之前,我应该简要地讲一下数据拆分。对于时间序列问题,随机拆分是不合适的。随机拆分数据会导致未来的数据点被包含在测试集中,从而给出虚假的(且可能过于乐观的)性能结果。我按日期拆分了数据,以防止任何泄漏,并设立了准确的验证。
-
对于 Iquitos,训练数据集是截至 2008 年 9 月 30 日的所有观测数据。对于 San Juan,拆分点设置在 2004 年 7 月 30 日。
-
为了进一步减少过拟合,我在训练模型时使用了 5 折时间序列交叉验证。 阅读更多关于时间序列交叉验证的详细信息。
超参数调整
我使用了均匀分布上的随机搜索来调整我的正则化参数。我发现这是一个明智的方法,可以快速探索广泛的超参数空间。阅读更多关于随机搜索的详细信息。
注意,我只调整了 l2 正则化。这是 Ridge 和 XGBoost 的 alpha 和 lambda 参数。
如果你想尝试不同的正则化参数,看看它们如何影响模型拟合,可以查看这个 我之前制作的 Streamlit 应用。
作者提供的图像:Iquitos 的 Ridge 模型超参数调整过程示例。平均测试分数为 MAE
损失函数
我选择的损失函数包括泊松回归、平均绝对误差 (MAE) 和均方误差 (MSE)。
泊松回归
对于登革热传播,独立假设是合理的,因为病例不会从一个人传到另一个人。然而,恒定速率假设可能不适用于本案例研究,我认为登革热病例的发生率会根据多维因素显著变化。然而,泊松分布确实以平均速率作为模型参数。
当目标变量是遵循泊松分布的计数数据时,使用泊松损失函数。这种分布类型假设事件独立发生并以恒定速率发生。
平均绝对误差
平均绝对误差(MAE)仅仅测量预测值与实际值之间的绝对差异。MAE 对异常值的敏感性低于一些其他损失函数,尤其是 MSE。
我认为许多人将损失函数与评分指标混淆,假设最佳的损失函数是 MAE,因为这是比赛评分的依据,你将看到事实并非如此。
均方误差
均方误差(MSE)计算模型输出与期望输出之间的平方差。由于平方差异,MSE 对异常值敏感。它对预测值和实际值之间的大差异惩罚比小差异更严重。
模型性能
模型性能在验证集上进行评估。评分(与损失函数不同)是基于竞赛中的 MAE 评分和易解释性(简单地衡量登革热病例)。你将从下面的图表中看到选择损失函数如何显著影响每个模型的数据拟合情况。在分析这些图表时,考虑一下选择的损失函数如何导致模型以这种方式拟合数据。
岭回归
伊基托斯
伊基托斯模型 MAE:7
该模型捕捉到了一些季节性变化,但未能有效预测异常值。
作者提供的图像:伊基托斯的岭回归模型预测值与实际值
圣胡安
圣胡安模型 MAE:23.7
该模型能够捕捉到一些季节性变化,但未能有效预测异常值。岭回归的预测值通常比实际病例略高。
作者提供的图像:圣胡安的岭回归模型预测值与实际值
XGBoost 回归器:泊松损失
伊基托斯
伊基托斯模型 MAE:7.8
该模型捕捉到了一些季节性变化,但未能有效预测异常值。模型预测到的峰值往往被低估。
作者提供的图像:伊基托斯的泊松 XGBoost 模型预测值与实际值
圣胡安
圣胡安模型 MAE:18.9
模型似乎能够很好地预测训练数据中的峰值。模型尝试对验证数据进行预测,但可能会出现过度预测。使用泊松损失训练使模型尝试预测峰值。
作者提供的图像:Poisson XGBoost 模型预测与 San Juan 实际数据对比
XGBoost 回归器:MAE 损失
Iquitos
Iquitos 模型 MAE: 7
尽管 MAE 较低,但该模型似乎只是通过数据绘制了一条直线。它对异常值和季节性完全不敏感。
作者提供的图像:MAE XGBoost 模型预测与 Iquitos 实际数据对比
San Juan
San Juan 模型 MAE: 16.6
该模型在预测季节性方面似乎有效,但在预测峰值方面表现不佳,尽管它确实尝试了这方面的预测。
作者提供的图像:MAE XGBoost 模型预测与 San Juan 实际数据对比
XGBoost 回归器:MSE 损失
Iquitos
Iquitos 模型 MAE: 7.3
该模型能够捕捉到一定的季节性,但在捕捉异常值方面表现不佳。
作者提供的图像:MSE XGBoost 模型预测与 Iquitos 实际数据对比
San Juan
San Juan 模型 MAE: 18.48
该模型捕捉了季节性,但对异常值的捕捉效果不是很好。
作者提供的图像:MSE XGBoost 模型预测与 San Juan 实际数据对比
提交结果
Poisson 模型在测试数据上获得了最高的整体准确率,MAE 为 27.6,MSE 模型次之,为 27.8,MAE 模型最后,为 29。
Poisson 损失 XGBoost 在竞争中排名前 27%。对于最小的超参数调整和特征工程,这个成绩还不错。
最后的思考
选择最佳模型完全依赖于预测目标。严格来说,如果我们只是讨论在验证集上最小化 MAE 的模型,那么整体最佳的是具有 MAE 损失的 XGBoost 回归器。然而,表现出最佳建模能力的模型似乎是 Poisson 损失变体。
感谢阅读。
[1] Bull, P., Slavitt, I. 和 Lipstein, G. (2016). 利用群众的力量增加社会部门数据科学的能力。 [在线] 可用: https://arxiv.org/abs/1606.07781 [访问日期:2023 年 3 月 13 日].
[## 通过我的推荐链接加入 Medium - John Adeojo
我分享数据科学项目、经验和专业知识,以帮助你在旅程中前行。你可以通过……
medium.com](https://medium.com/@johnadeojo/membership?source=post_page-----77cf69dc9d2f--------------------------------) [## 主页 | John Adeojo
关于我 欢迎来到我的专业作品集!我是一名经验丰富的数据科学家和机器学习(ML)专家……
《人工智能中的持续学习现状》
为什么 ChatGPT 只训练到 2021 年?
·
关注 发表在 Towards Data Science ·23 分钟阅读·2023 年 10 月 18 日
--
图片由作者使用 DALL-E 3 生成
知识前提:
几年前,我通过 StatQuest 视频、Lena Voita 的 NLP 博客以及《深度学习实践者》和《谈谈网络》等书籍学习了深度学习的基础。我现在希望了解深度学习中的持续学习的现状。我发现很少有信息能以简单的术语总结这个主题,并且需要筛选专家研究论文。因此,这篇文章是为那些对该主题有基本了解但发现研究难以阅读且可能不是专家的读者准备的。它集中在聊天机器人上,因此了解 chatGPT 的训练阶段也很有帮助。
简介
ChatGPT 告诉用户它的训练只到 2021 年 9 月(作者截图)
如果像 ChatGPT 这样的语言模型能够不断用新数据更新,它们将加速从软件开发到法律程序到学习的一系列任务。这也会使像这样的一些文章变得过时。
持续学习是暂停模型训练过程,保存模型当前状态,然后稍后在新数据上重新开始训练的能力。模型应该能够很好地泛化到新数据,同时仍保持对旧数据的泛化能力。有关更正式的定义,请参阅这篇论文。
目前,行业内增强聊天机器人的数据趋势是使用 RAG,将查询的向量与提示工程相结合来回答问题,而不是继续用新数据训练 LLM。ChatGPT 的零-shot 学习能力,使其能够回答关于新、未见数据的问题,这种方法非常有吸引力。例如,你可以教它一种新的编程语言,然后用少量提示询问有关该语言的问题,尽管性能会随着输入的 tokens 数量增加而略微下降。基于新主题不断训练模型回答问题需要大量计算资源,更重要的是,需要广泛的相关主题数据。此外,如果一个主题在训练集中出现频率很低,它的泛化能力会很差。例如:拿一个不受欢迎的公共仓库,尽管它在训练过程中曾见过,但可能对它了解甚少,还可能出现幻觉。上下文窗口(模型能接收的 tokens 数量)正在迅速变大,使 RAG 更加吸引人。然而,理想情况下,我们是否不希望有一个智能的全知模型,而不需要任何外部数据库?
持续学习是通向 AGI 的重要一步,有人怀疑我们是否能够在没有深度学习网络架构重大变革的情况下实现这一目标。杰夫·霍金斯在他的书《千脑》中提到,他认为当前的 ANN 不具备有效的持续学习能力,并且认为未来的模型可能需要更类似于人脑的架构,利用他关于新皮质皮层柱中的参考框架的理论。
语言模型的预训练阶段与微调阶段的持续学习
今年早些时候,发布了一篇名为《LIMA:少即是多的对齐》的研究论文。该论文介绍了一个没有使用来自人类反馈的强化学习(RLHF)进行训练的聊天机器人,而是仅在 1,000 个精心标注的问答样本上进行了微调。令人惊讶的是,研究人员表示,在 43%的情况下,“该聊天机器人的回应与 GPT-4 相当”。我没有深入了解这些回应是如何评估的,但尽管如此,普遍认为模型的大部分知识和能力是在预训练阶段获得的,这项研究进一步证明了这一点。
像 ChatGPT 和 Llama-chat 这样的模型已经经历了广泛的微调,以生成更对齐和有效的响应。OpenAI 目前提供一个 API 来进一步微调模型,该 API 接受问答数据作为输入用于进一步训练。然而,这不应被用来教模型新数据,而是用来定制语气和可调性。尝试通过微调模型来教它新数据可能会导致灾难性遗忘,即模型会忘记已学到的内容。本文将介绍一些旨在减轻这一问题的技术。
这也引出了关于持续学习的可行性和策略的一些关键问题:
-
在哪个阶段引入持续学习最为有利且最为容易?
-
鉴于微调和 RLHF 都会改变整个模型的参数,是否还有可能回到预训练阶段进行进一步修改?
注:我为以下讨论的部分论文提供了一些类似 PyTorch 的伪代码。这些伪代码未经过测试,可能无法正常工作,用于逐步解析技术并翻译任何混淆的数学符号,以帮助读者理解。
持续学习技术的 5 个子类别
持续学习的综合概述论文指出,持续学习的训练策略可以分为 5 个子类别:
-
基于正则化的方法:这种方法在训练过程中添加约束或惩罚。
-
基于优化的方法:这种技术侧重于修改优化算法。
-
基于表示的方法:这旨在学习跨不同任务的共享特征表示,帮助模型更好地推广到新的但相关的任务。
-
基于回放的方法:这涉及存储先前任务的一些数据或学习特征,并在训练新任务时重播它们,以维持对早期学习任务的性能。换句话说,在训练新任务时混合旧数据集和新数据集。
-
基于架构的方法:在这种方法中,网络架构是动态调整的,通常通过增长或分区来委派网络的不同部分给不同的任务。
1. 基于正则化的方法
参数的软掩蔽
下面的软掩蔽技术在训练过程中对每个参数的梯度进行掩蔽和调整。接下来的基于优化的方法也会操纵梯度以实现持续学习。记住,梯度不只是在训练期间出现和消失的临时数字,它们是指导权重演变的信号。
SPG
本论文 提出了一种名为 SPG(Soft-masking of Parameter-level Gradient flow)的技术,旨在:
-
在每个任务上训练模型直至收敛。
-
训练后,计算每个任务的每个参数的“重要性”。
-
根据其累积重要性软掩蔽参数,使重要参数在学习新任务时更不可能改变。
让我们逐步分解这个方法:
1. 训练第一个任务
如常在第一个任务的数据集上训练模型。
2. 计算第一个任务的参数重要性
在第一个任务的训练完成后,我们计算每个模型参数的重要性。这里的直觉很简单,我们使用每个参数的梯度来计算其重要性。较大的梯度意味着该参数的微小变化将导致损失更大的变化,这意味着模型的性能可能会更显著地变化,因此该参数是重要的。
梯度也被归一化,因为第一层的梯度可能很小,而最后一层的梯度可能很大。如果你基于这些原始梯度值计算重要性,最后一层的参数会显得更重要,因为其梯度的规模,并非因为它们在任务中真正更关键。
计算 SPG(详见论文 第 3.1 节)中模型参数重要性的方程式
让我们将这个计算翻译成类似于 PyTorch 的伪代码:
import torch
def compute_final_importance(model, loss_function, data_loader):
# Get a single batch from the data loader
inputs, labels = next(iter(data_loader))
# Forward and backward pass to calculate the gradients for all parameters
outputs = model(inputs)
loss = loss_function(outputs, labels)
loss.backward()
importances = []
# Calculate importance based on the gradients
for param in model.parameters():
if param.grad is not None: # Gradients may be None for some unused parameters
normalized_grad = (param.grad - torch.mean(param.grad)) / torch.std(param.grad)
importance = torch.tanh(normalized_grad)
importances.append(importance)
return torch.stack(importances).mean(dim=0)
3. 跨任务积累重要性
每个参数在任务之间的累计重要性是通过在任何阶段取最大值来简单计算的。
4. 训练后续任务、组合损失和软掩码机制:
在新任务上进行训练时,研究人员使用一个由两个部分组成的组合损失函数。其中一个是标准损失函数,用于新任务和数据,另一个是额外的损失函数,它涉及将新数据通过旧模型(即上一个任务之后的收敛模型检查点),并汇总生成的 logits。在分类网络中,logits 通常是模型在通过类似 softmax 函数之前生成的原始非标准化预测。这些 logits 的总和作为一种损失形式。其理由是,如果在模型参数发生变化时,总和 logits 受到显著影响,那么这些参数对之前学习任务的性能至关重要。
这些额外损失生成的梯度在反向传播过程中作为指导,推动共享参数朝着一个不容易损害第一个任务性能的方向改变。因此,它充当了一种惩罚项,以强制确保对模型所做的任何更新不会导致与先前任务相关的重要信息的显著丧失。
在下一个任务上训练模型。使用标准训练循环,但在反向传播过程中,根据梯度的累计重要性来修改梯度。这就是软掩码机制:
import torch
accumulated_importance = # calculated at the end of each task
for epoch in range(num_epochs):
for x, y in train_loader:
# Forward Pass: Calculate the loss for the current task using the proper loss function
logits = new_model(x)
loss_current_task = nn.CrossEntropyLoss()(logits, y)
# Forward Pass: Calculate the additional losses for previous tasks (CHI mechanism)
loss_previous_tasks = 0
for prev_task_id in range(task_id):
logits_prev = old_model(x, prev_task_id)
loss_previous_tasks += logits_prev.sum()
# Combine the losses
combined_loss = loss_current_task + loss_previous_tasks
# Backward Pass
optimizer.zero_grad()
combined_loss.backward()
# Update the accumulated importance
for param, acc_imp in zip(model.parameters(), accumulated_importance):
grad = param.grad
acc_imp = torch.max(acc_imp, torch.abs(grad))
# Soft-masking the gradients before taking an optimization step
for param, imp in zip(model.parameters(), accumulated_importance):
param.grad *= (1 - importance)
optimizer.step()
5. 软掩码特殊情况
-
特征提取器:共享特征提取器中参数的梯度根据其特定的累计重要性进行修改。
-
分类头:对于分类头,梯度根据特征提取器的平均重要性进行修改。
将其应用于大型语言模型(LLMs)
请注意,这篇论文没有用语言模型进行实验,但我假设在语言模型中,你可以将变换器层视为类似于“特征提取器”,将最终分类层(预测序列中的下一个词或标记)视为“分类头”。
应用于语言模型持续预训练的软掩码
接下来我们将讨论一篇将类似软掩码应用于语言建模中的预训练阶段的论文。
这篇论文介绍了一种称为 DAS(带软掩码的持续 DA 预训练)的方法,用于大型语言模型预训练阶段的持续学习。它应用了一种类似于刚才讨论的软掩码技术,并结合了其他几种技术,试图在不遇到灾难性遗忘的情况下继续对 LLM 进行预训练。
让我们一步步分解:
初始预训练阶段
像正常一样预训练 LLM。
在新领域的进一步预训练
准备新领域数据:
准备一个来自不同领域的新数据集。
计算每个神经元的重要性
SPG 使用梯度来确定每个参数的重要性,然后将计算得到的重要性值应用于训练过程中参数梯度调整的掩码。本文试图确定每个单元/神经元的重要性,而不是参数,然后在训练过程中以相同的方式使用这种重要性,通过掩盖梯度来进行训练。
本文使用两种不同的方法来计算神经元的重要性,具体取决于任务。其一是基于梯度的重要性检测方法(最初在这篇论文中概述),其二是定制的“代理损失函数”。
首先介绍的是不是用于第一个新领域的持续学习。为什么?它需要来自训练数据集的数据才能工作,而作者表示用户“无法访问庞大的原始预训练数据集”,这是一个合理的假设。因此,在持续学习的第一个阶段使用的是代理损失函数,然后在随后的每个阶段使用其他方法。
代理损失函数(“代理 KL 散度损失”):
我最初觉得这个术语令人困惑,但它之所以这样称呼,是因为原始的基于梯度的重要性检测方法本身被定义为一种损失函数,你可以用它来运行网络的输出以获得每个神经元的梯度,然后可以用这些梯度来推导重要性,就像 SPG 技术一样。计算方法如下:
-
取出我们希望训练的新领域的一个子集,并将其通过模型输入两次,以获得两个不同的表示。这些表示会有所不同,因为 Transformer 架构中存在的 dropout 掩码。
-
计算这两个表示之间的 KL 散度。
使用代理和综合损失的修改版反向传播流程
-
前向传播: 数据通过神经网络进行前向传播。
-
反向传播:
应用代理损失以调整梯度: 代理损失函数的单元级重要性用于软掩盖原始梯度。这可以表示为:
adjusted_grad *= (1 − unit_level_importance)
计算综合损失(MLM + 对比损失): 使用 MLM 和对比损失来计算综合损失。
在更多领域进行进一步预训练
-
直接重要性计算: 对于每个新领域,现在可以直接使用来自新领域的数据通过方程 3 中概述的基于梯度的方法来计算每个单元的重要性,从而消除了在初始预训练后仅使用一次的代理损失函数的需要。
-
神经元的重要性在每个新任务学习时逐步更新。 这种更新使用逐元素最大值。“逐元素最大(EMax)操作”指的是逐个元素比较两个向量,并取每个对应元素的最大值来创建一个新向量。例如:如果你有两个相同长度的向量 A 和 B,逐元素最大值将产生一个新向量 C,其中每个元素 C[i] 是 A[i] 和 B[i] 之间的最大值。
2. 基于优化的方法
我们将参考综合调查论文第 3.1 节中概述的两种技术。
梯度方向保持
论文讨论了操控基于梯度的优化过程,以使新训练样本的梯度方向接近旧训练样本的梯度。公式
⟨ ∇θ Lₖ(θ; Dₖ), ∇θ Lₖ(θ; Mₜ) ⟩ ≥ 0
强制要求学习新任务时不会增加旧任务的损失。本质上,鼓励新任务和旧任务的梯度对齐。
分解公式时,我们取新任务的损失梯度(∇θ Lₖ(θ; Dₖ))与旧任务的损失梯度(∇θ Lₖ(θ; Mₜ))的点积,应该是非负的。在这种情况下,正的点积意味着旧任务和新任务的梯度通常指向相同的方向,这两个向量之间的角度小于或等于 90 度。
前向/反向传播:
前向传播:
你需要将新任务的输入数据 Dₖ 和旧任务的 Mₜ 通过同一个模型来计算每个任务的损失。
反向传播:
-
计算旧任务和新任务的网络参数的损失梯度。
-
对齐检查:计算两个梯度的点积。然后使用这些信息以使新任务的梯度的点积为非负。
-
更新权重:使用这些“对齐”的梯度更新模型参数。
import torch
# Forward pass for the new task
output_k = model(D_k)
loss_k = criterion(output_k, y_k)
# Forward pass for the old task
output_t = model(M_t)
loss_t = criterion(output_t, y_t)
# Compute gradients for both tasks
loss_k.backward(retain_graph=True) # Compute gradients for new task but keep computation graph
grad_k = torch.cat([p.grad.view(-1) for p in model.parameters()])
optimizer.zero_grad()
loss_t.backward() # Compute gradients for old task
grad_t = torch.cat([p.grad.view(-1) for p in model.parameters()])
# Compute dot product and modify gradients if they don't align
dot_product = torch.dot(grad_k, grad_t)
if dot_product < 0:
# I'm not sure how you modify the gradients here if they don't align, I'm not sure the paper specifies it
# Use the modified gradient to update model parameters
index = 0
for p in model.parameters():
num_params = p.numel()
# Update using modified gradients
p.grad = grad_k[index: index + num_params].view(p.shape)
index += num_params
optimizer.step()
无需旧训练样本的梯度方向保持
文本还强调,即使不存储旧样本,也可以进行梯度投影。NCL(自然连续学习,论文链接)是这里总结的技术。请注意,这可以归类为正则化和优化两种方法。
训练过程逐步:
前向传播:
你需要将新数据通过网络并计算损失。
反向传播:
目标: 目的是在遵守距离约束 d(θ,θ+δ)≤r. 的同时,最小化任务特定的损失 ℓk(θ)。
算法逐步:
-
如常,计算模型参数∇θℓk(θ)的损失梯度。
-
δ 是使用更新规则计算的。这为你提供了基于新任务要求的模型参数 θ 的“建议”更改。
-
然后,将这个 δ 插入到距离约束公式中:d(θ,θ+δ)=squareroot(δ⊤Λ_k-1δ)。约束在当前参数 θ 周围起到边界作用,由距离度量 d(θ,θ+δ) 和半径 r 定义。我很难理解为什么叫它“半径”,而不是“约束值”或其他什么。我认为这是因为研究人员在高维空间中可视化梯度和训练过程。当你基于距离度量应用约束时,本质上是在高维空间中定义了一个围绕当前参数值的“球体”。这个球体的“半径” r 设置了在学习新任务时参数可以移动的限制。
-
如果提议的 δ 根据这个距离度量将 θ 移动得过远,即超出了这个边界,你就将其缩小,以使其保持在由半径 r 定义的允许区域内。
让我们更深入地查看每一部分:
更新规则: 更新规则提供了 θ 应该移动的方向。
来自 持续学习论文的综合概述 第 3.1 节的 NCL 更新规则
分解如下:
-
∇θ ℓk(θ) 表示由损失函数计算的所有参数 (θ) 的梯度。
-
参数重要性计算 (Λ^(k-1)_(-1)): 这个术语表示一个 精度矩阵,它是计算网络中参数重要性的另一种方式。更多细节见下
-
正则化项 (θ — μ_(k-1)): 这个项将更新后的参数拉近到来自先前任务的最佳参数 μ_(k-1)。像之前的方法一样,它作为正则化器以避免偏离已经学到的内容。
-
学习率 (λ)
距离约束: 在应用这个更新之前,你通常会检查这个变化 δ 是否会违反距离约束 d(θ,θ+δ)≤r。如果违反了,你通常会缩小 δ,以使其满足约束。
精度矩阵解释: 在之前的软掩蔽方法中,我们通过所有神经元的输出或它们的梯度来计算重要性。在这个方法中使用了一个精度矩阵。这有点复杂,所以我会尽力解释:
我们首先计算网络参数的 协方差矩阵。在神经网络的上下文中,梯度矩阵 G 的列对应于模型的参数(权重和偏差)。G 的每一行代表一个训练样本的梯度向量,相对于所有这些参数。
如果你有一个具有 P 个参数的神经网络(这包括所有层的所有权重和偏置),那么每个梯度向量将有 P 个元素,每个元素对应一个参数。因此,G 将是一个形状为 N × P 的矩阵,其中 N 代表每个批次,因此每行代表在给定批次中的所有训练样本的平均梯度向量。
当你从 G 计算协方差矩阵 Σ 时,得到的矩阵将具有 P × P 的维度。对角线条目 Σii 将指示与 ith 参数相关的梯度的方差,而非对角线条目 Σij 将指示与 ith 和 jth 参数相关的梯度之间的协方差。这让你了解这些参数在训练过程中如何相互作用或协变。该矩阵的逆矩阵是 精度矩阵,我们用它来确定重要性。
为什么选择 精度矩阵 而不是 协方差矩阵?虽然协方差矩阵 Σ 确实捕捉了参数在训练过程中如何相互作用,但它没有特别指示每个参数在考虑所有其他参数时对当前任务的关键程度。相比之下,精度矩阵允许我们评估 条件独立性(这是概率论中的一个概念,查阅一下)。精度矩阵中的大值表示,在给定所有其他参数的情况下,知道一个参数对另一个参数的了解具有很高的信息量。我不会进入如何运作的示例,所以请让 ChatGPT 生成一些示例,使用一个非常小的神经网络来看看这些值如何被解释。
我们之前看到的计算重要性的方法关注于单个神经元或参数,忽略了它们之间的关系。另一方面,精度矩阵可以捕捉这些关系。像深度学习中的一切一样,这是否是一种更好的计算网络重要性的方法,还需要通过经验来验证,并且可能会根据任务和网络规模的不同而有所不同。
PyTorch 中的算法步骤:
import torch
# Constraint radius
radius = 0.1
for epoch in range(num_epochs):
for batch_idx, (data, target) in enumerate(data_loader):
optimizer.zero_grad()
# Forward pass
output = model(data)
loss = loss_function(output, target)
# Backward pass to get gradients for params
loss.backward()
model_grad = torch.cat([p.grad.data.view(-1) for p in model.parameters()])
# Compute δ using the NCL method
# δ = Λ^(-1) * grad - (θ - µ)
delta = torch.matmul(torch.inverse(covarianceMatrix), model_grad) - (torch.cat([p.data.view(-1) for p in model.parameters()]) - parametersForPrevTask)
# Check constraint
if torch.norm(delta) > radius:
delta = radius * delta / torch.norm(delta)
# Update model parameters (θ) using δ
idx = 0
for p in model.parameters():
length = p.data.numel()
p.data += delta[idx: idx + length].view(p.data.shape)
idx += length
# Update Λ and µ for the next task, probably going to be task-specific and non-trivial
3. 基于表示的方法
首先,需要注意的是,对 LLM 进行预训练以便进一步在下游任务中进行微调,是这个子类别中的持续学习的一个例子。我认为 ChatGPT 在处理从未见过的数据时的推理能力也是这种方法的一个例子。虽然我们从技术上称之为零样本学习,而“持续学习”这个术语要求更新模型参数,但它超越了我们以前见过的任何东西。正如介绍中所讨论的,提示工程可能是持续学习的未来,而不是不断更新参数。
接下来我们将探讨使用知识蒸馏进行持续学习。我不太确定这属于哪个子类别,但我猜这可能是表示、架构和重放方法的混合。尽管我们正在审查的一些技术可能看起来随意且在大规模上未经验证,但这一领域的突破往往是不可预测的。因此,保持广阔的视角非常重要。
知识蒸馏用于持续学习
你可以将一个网络的知识(或“蒸馏”)转移到另一个网络中,第二个网络能够合理地近似原始网络所学习的功能。
蒸馏模型(学生)被训练来模仿较大网络(教师)的输出,而不是直接用原始数据进行训练。例如,假设你想训练一个较小的学生模型来模仿一个大型预训练语言模型(教师)。将原始预训练数据集通过教师模型生成“软目标”。这些是潜在输出的概率分布,例如:下一词预测任务中,教师可能会提供像 90% 为“cat”、5% 为“kitten”、3% 为“feline”等概率。
这通常是为了将知识转移到更小的模型上,并且尽管模型较小,但效果非常好。
让我们看看一些研究人员是如何成功应用到一个 NER(命名实体识别)模型上的。训练过程非常简单:
训练过程一步步进行
论文中概述了两种主要方法:AddNER 和 ExtendNER。
AddNER 模型
请注意,NER 模型通过接收一系列标记(通常是一个句子)作为输入,然后为每个标记输出一个概率分布(针对不同类型的实体)。IOB 标记法通常用于 NER 模型,每个标记可以被标记为‘O’,或作为实体类型 X 的开始(‘B-’)或内部(‘I-’)。‘O’表示‘Outside’,这意味着当前标记不属于任何实体。因此,对于 n 个实体类型,你将在分类层中得到 2n 个输出神经元:n 个用于‘B-’ 标签(每种实体类型一个),n 个用于‘I-’ 标签(同样,每种实体类型一个)。加上‘O’ 标签,表示一个标记不属于任何实体,你将得到 2n + 1 个可能的标签。最终的维度可以写成 h × (2n + 1),其中 h 是隐藏层输出的大小。请注意,这仅适用于标记只能属于一个实体的模型。例如:“Apple” 可以被标记为“FOOD”和“COMPANY”。
架构和师生设置
在这种情况下,学生模型是教师模型的一个副本,额外增加了一个输出分类层,以学习模型需要识别的每种新实体类型。在训练过程中,新的输出层从新的标注数据中学习,而旧的层则通过教师模型的输出进行指导,以减少遗忘。
训练后,旧的输出层不会被丢弃。它接着使用冲突解决器部分(第 3.3 节末尾)中描述的算法和启发式方法,将这些输出组合成每个序列中的单一最终预测。
AddNER 模型的示意图来自第 3.2 节
前向传播
-
旧实体类型:将输入句子通过教师模型,以获得旧实体类型的概率分布(在此背景下为“软目标”)。
-
新实体类型:相同的句子也会通过新学生模型,该模型具有专门针对新实体类型的额外输出层。
后向传播
组合损失函数:
-
KD 损失:通过比较新模型(学生)对旧实体类型的输出概率与旧模型(教师)的匹配程度来计算。它使用 KL 散度来计算。它可能是逐个标记计算的,然后在句子或批次中的所有标记上求和或取平均,但我认为论文没有详细说明这一点。
-
交叉熵损失:这是常用的损失函数,用于比较模型对新实体类型的预测与来自新数据集的实际标签。
-
两者结合:这两种损失通过加权求和的方式结合成一个组合损失。结合这些损失的权重由超参数 alpha 和 beta 设定,像调整其他超参数一样,根据实验结果优化性能。
# Hyperparameters alpha and beta for weighting the two loss functions
alpha = 0.5
beta = 0.5
for epoch in range(num_epochs):
for sentence, labels in D_new:
# Forward pass in teacher model for old entity types
teacher_probs_Ei = teacher_model(sentence)
# Forward pass in student model for old and new entity types
# Note: the new entity types must go through the new output layer (not shown in this pseudocode)
student_probs_Ei, student_probs_Enew = student_model(sentence)
# Compute KD loss
kd_loss = KL_divergence(teacher_probs_Ei, student_probs_Ei)
# Compute CE loss for new entity types
ce_loss = cross_entropy(labels, student_probs_Enew)
# Combined loss
total_loss = alpha * kd_loss + beta * ce_loss
# Backward pass
total_loss.backward()
# Update student model parameters
optimizer.step()
ExtendNER 模型
架构和师生设置
ExtendNER 模型扩展了输出层的维度,以适应新的实体类型,而不是添加新的输出层。论文相当简单地解释了这些维度应如何设置:
“假设Mi能够识别n个实体类型,它的最终层可以被视为一个h×(2n+1)的矩阵。然后,Mi+1的输出层将扩展为一个h × (2n + 2m + 1)的矩阵,以适应新的实体类型。”
ExtendNER 模型的示意图来自第 3.4 节
前向传播
与 AddNER 相同,但维度扩展。
后向传播
损失计算使用KL 散度损失或交叉熵损失,具体取决于以下因素:
-
当 NER 类别标签y为“O”(来自 IOB 标注模式)时,使用 KL 散度损失。
-
当类别标签y不是“O”时,使用交叉熵损失。
最终预测
Viterbi 算法用于解码最终的实体类型。
AddNER 和 ExtendNER 模型在持续学习中表现良好,结果之间差异不大
4. 基于回放的方法
“微调的语言模型是持续学习者”
论文中的模型不是像 GPT 这样仅用于对话回应的通用单任务模型。相反,它是为一系列专业任务进行微调的,从文本简化到俳句生成。这些任务各自有独特的要求、评估标准和专业训练数据集。
研究人员将旧数据集的部分与新数据集混合,通过在微调新任务时只混入 1%的旧任务数据集,取得了很好的效果。这是在多个任务上顺序进行的(8)。该模型在零-shot 学习设置中也表现良好,意味着它可以很好地推广到未经过训练的任务。例如,当给定一个未见过的主题时,它可以生成具有正确音节数的俳句,显示出其推广能力。研究人员还提到,他们的方法是任务顺序不变的,意味着学习任务的顺序不会影响模型的表现。实验发现,与新数据集混合的旧数据集的数量不会显著影响主要任务的表现。然而,它会影响零-shot 学习。在 0%复习时,模型倾向于忘记零-shot 任务,而在 1%复习时,模型在这些任务中的表现保持得很好。
这些都显得积极,我们可以只添加 1%的旧数据集并解决持续学习的问题,但当然,将其应用于像 chatGPT 这样的聊天机器人,将是经验性的,并且可能完全不同。即使假设 chatGPT 可以在微调和 RLHF 阶段持续训练,也需要大量标记的对话数据。
5. 基于架构的方法
我不会在这里详细讨论具体的论文或实现,但我会简要概述这种方法和几种不同的技术。我建议阅读这篇综合调查论文的第 4.5 节。它比其他章节更容易阅读。
-
参数分配:在这里,网络参数的一个子集被分配给每个任务。这可以通过屏蔽无关的神经元或明确识别当前任务的重要神经元来实现。
-
模块化网络:这涉及为每个任务使用独立的子网络或模块。
子网络可以以各种方式连接,以形成一个集成体或更复杂的架构。以下是几种常见的子网络连接方法:
输出的连接:
在这种方法中,多个子网络的输出被串联成一个张量,然后可以通过额外的层处理以生成最终输出。
投票机制:
在某些模型中,每个子网络对可能的结果进行“投票”,最终决定通过多数投票或加权投票来做出。这有生物学启发,因为它类似于新皮层中不同皮层柱进行投票的方式。
跳跃连接:
一些架构允许子网络具有跳跃连接到模型的其他部分,从而允许信息在模块之间流动。
顺序:
在这种情况下,一个子网络的输出作为下一个子网络的输入。
回到聊天机器人,如果能创建一个由两个子网络组成的架构,我觉得特别有趣。第一个是预训练模型,它包含通用的“知识”。第二个包含对齐模型所需的知识。一旦模型对齐,它将不再需要标记的对话数据。相反,它可以通过以无监督的方式训练预训练子网络来不断更新。
结论
总之,深度学习中持续学习的子领域具有挑战性,并且大部分仍未被了解。这是因为我们尚未完全理解大规模语言模型中的神经元如何工作,正如引言中所述,当前的网络架构或深度学习本身可能不适合此任务。
我注意到上个月 ChatGPT(仅 GPT-4)已经更新,现在它显示“自我的训练截止日期是 2022 年 1 月”,所以我想知道 OpenAI 的团队是如何实现这一点的。
ChatGPT(GPT-4 变体)告知用户其训练数据截至到 2022 年 1 月(作者截图)
《维度灾难揭秘》
原文:
towardsdatascience.com/the-curse-of-dimensionality-demystified-2fc9b0bb1126
理解维度灾难背后的数学直觉
·发布于 Towards Data Science ·23 分钟阅读·2023 年 10 月 6 日
--
图片来源: pixabay.com/illustrations/ancient-art-background-cosmos-dark-764930/
维度灾难 指的是在分析高维数据时出现的问题。数据集的维度或维数指的是数据集中线性独立特征的数量,因此高维数据集就是特征数量众多的数据集。这个术语首次由贝尔曼在 1961 年提出,他观察到,为了以一定的准确性估计一个任意函数,所需的样本数量会随着函数所取参数数量的增加而呈指数增长。
在这篇文章中,我们详细探讨了在分析高维数据集时出现的数学问题。尽管这些问题可能看起来违反直觉,但可以用直观的方式来解释它们。我们不进行纯理论讨论,而是使用 Python 创建并分析高维数据集,观察维度灾难在实践中的表现。在这篇文章中,所有图片,除非另有说明,均由作者提供。
数据集的维度
如前所述,数据集的维度定义为其具有的线性独立特征的数量。一个线性独立的特征不能表示为数据集中其他特征的线性组合。因此,如果数据集中的一个特征或列是其他特征的线性组合,它不会增加数据集的维度。例如,图 1 显示了两个数据集。第一个数据集有两个线性独立的列,因此其维度为 2。在第二个数据集中,一列是另一列的倍数,因此我们只有一个独立特征。正如该数据集的图示所示,尽管有两个特征,但所有数据点都在一条 1 维的线上。因此,这个数据集的维度是 1。
图 1
维度对体积的影响
维度诅咒的主要原因是维度对体积的影响。在这里,我们关注数据集的几何解释。通常,我们可以假设数据集是从一个群体中抽取的随机样本。例如,假设我们的群体是图 2 所示的正方形上的点集合。这个正方形是所有点(x₁, x₂)的集合,使得 0≤x₁≤1 和 0≤x₂≤1,且这个正方形的边长为 1。
每个群体都有其自己的分布,这里我们假设这个正方形上的点是均匀分布的。这意味着如果我们试图从这个群体中随机选择一个点,所有的点都有相等的被选中机会。现在,如果我们从这个群体中抽取大小为n的随机样本(即从这个正方形中随机选择n个点),这些点形成了一个具有n行和两个特征的数据集。因此,这个数据集的维度是 2。
图 2
同样地,我们可以通过从一个边长为 1 的立方体中抽取随机样本来创建一个维度为 3 的数据集(图 2)。这个立方体是所有点(x₁, x₂, x₃)的集合,使得 0≤x₁≤1, 0≤x₂≤1, 0≤x₃≤1,并且这个立方体上的点是均匀分布的。
最终,我们可以扩展这个想法,通过从边长为 1 的d维超立方体中随机抽取大小为n的样本来创建一个d维数据集。超立方体定义为所有点(x₁, x₂,…, x_d)的集合,使得 0≤xᵢ≤1(对于 i=1…d)。同样,我们可以假设这些点在这个超立方体中是均匀分布的。生成的数据集包含n个示例(观测值)和d个特征。
边长为L的d维超立方体的体积是L^d。(请注意,如果d=2,该公式给出的就是边长为L的正方形的面积。然而,这里我们假设面积是二维对象的体积的特例)。因此,如果L=1,不管d的值是什么,体积都是 1。但重要的不是超立方体的总体积,而是体积在超立方体内部的分布。这里我们用一个例子来解释它。
我们可以将单位正方形分成 10 个壳体,如图 3 所示。第一个壳体实际上是单位正方形中心的小正方形,边长为 0.1。这个正方形的右下角位于x₁=0.55。第二个壳体的右下角位于x₁=0.6,因此其厚度为 0.05。所有其余的壳体都有相同的厚度,最外层壳体的右下角位于x₁=1。因此,它覆盖了单位正方形的边缘。图 3 展示了其中一个壳体作为示例,其右下角位于x₁=0.8。
图 3
我们可以轻松计算出底部右角位于x₁=c的壳体的体积(面积):
现在我们可以将此过程扩展到d维超立方体。我们将超立方体分成 10 个厚度相同的超立方壳体。
角落之一位于x₁=c的壳体的体积由以下公式确定:
列表 1 计算了不同d值下所有这些壳体的体积,并创建了体积的柱状图。结果如图 4 所示。
# listing 1
import numpy as np
import matplotlib.pyplot as plt
from scipy.stats import uniform, norm
from scipy.spatial import distance_matrix
from sklearn.neighbors import KNeighborsRegressor
%matplotlib inline
num_dims=[2, 3, 9, 15, 50, 100]
x_list = np.linspace(0.5, 1, 11)
edge_list = 2*(x_list - 0.5)
fig, axs = plt.subplots(3, 2, figsize=(14, 19))
plt.subplots_adjust(hspace=0.3)
for i, d in enumerate(num_dims):
vols = [(edge_list[i+1])**d-(edge_list[i])**d \
for i in range(len(x_list)-1)]
axs[i//2, i%2].bar(x_list[1:], vols, width = 0.03)
axs[i//2, i%2].set_title("d={}".format(d), fontsize=20)
axs[i//2, i%2].set_xlabel('Shell corner $x_1$ coordinate', fontsize=18)
axs[i//2, i%2].set_ylabel('Volume', fontsize=18)
axs[i//2, i%2].set_xticks(x_list)
axs[i//2, i%2].tick_params(axis='both', labelsize=12)
plt.show()
图 4
正如你所见,随着维度的增加,内层壳体的总体积份额迅速减少。事实上,对于d≥50,最外层壳体(其角落位于x₁=1)占据了单位超立方体总量的 99%以上。因此,我们得出结论,在高维超立方体中,几乎所有的体积都靠近超立方体的面,而不在其内部。
现在让我们计算在每个壳层中找到一个数据点的概率。记住我们假设这些超立方体中的点是均匀分布的。随机数据点在某个壳层内的概率可以通过对该壳层体积上的均匀分布概率密度函数(PDF)进行积分来计算。单位超立方体上的连续均匀分布的 PDF 是:
因此,随机数据点位于其中一个壳体(记作S)内的概率等于该壳体的体积:
因此,根据图 4 的结果,如果我们从高维超立方体中随机抽取一个数据点,它很可能会位于外层壳。对于 d≥50,这个随机数据点位于内层壳的概率几乎为零。
我们还可以使用 Python 确认这些结果。列表 2 从定义在 d 维单位超立方体上的均匀分布中抽取了 5000 个样本。然后,它计算了每个壳层中的数据点数量,并创建了它们的条形图。它使用了与列表 1 中相同的 d 值。
# Listing 2
np.random.seed(0)
num_dims=[2,3,9, 15, 50, 100]
x_list = np.linspace(0.5, 1, 11)
x_list[-1] = 1.001
x_list1 = np.linspace(0.5, 0, 11)
x_list1[-1] = -0.001
fig, axs = plt.subplots(3, 2, figsize=(14, 19))
plt.subplots_adjust(hspace=0.3)
for i, d in enumerate(num_dims):
sample = uniform.rvs(size=5000*d).reshape(5000, d)
count_list=[]
for j in range(len(x_list)-1):
inshell_count = (((x_list[j] <= sample) & \
(sample < x_list[j+1])) | \
((x_list1[j+1] < sample) & \
(sample <= x_list1[j]))).sum(axis=1)
outshell_count = ((x_list[j+1] <= sample) | \
(x_list1[j+1] >= sample)).sum(axis=1)
count = ((inshell_count>0) & (outshell_count==0)).sum()
count_list.append(count)
axs[i//2, i%2].bar(x_list[1:], count_list, width = 0.03)
axs[i//2, i%2].set_title("d={}".format(d), fontsize=20)
axs[i//2, i%2].set_xlabel('Shell corner $x_1$ coordinate', fontsize=18)
if i%2==0:
axs[i//2, i%2].set_ylabel('Number of data \npoints within shell',
fontsize=18)
axs[i//2, i%2].set_xticks(np.linspace(0.5, 1, 11))
axs[i//2, i%2].tick_params(axis='both', labelsize=12)
plt.show()
图 5
图 5 中显示了图形,如你所见,条形图的形状类似于图 4。每个壳层中的数据点数量与方程 1 中给出的壳层的相应概率成比例,因此它们与该壳层的体积成比例。实际上,当 d=100 时,几乎所有的数据点都包含在最外层壳中,这体现了维度的诅咒。
在高维超立方体中,几乎所有随机样本的观察点都接近面(图 6)。如果超立方体是 d 维的,这些观察点也可以代表一个具有 d 特征的数据集。这样一个数据集显然不能代表其抽样的总体。这里的总体是超立方体中所有数据点的集合,但我们的样本仅包含接近超立方体面上的数据点。在实际中,我们没有来自超立方体中心附近区域的数据点。假设我们使用这样的样本作为机器学习模型的训练数据集。模型在训练过程中从未见过接近超立方体中心的数据点,因此对这样的数据点的预测不会可靠。实际上,模型无法将其预测推广到这样的数据点。
图 6
我们还可以找到这些结果的不同解释。假设我们在区间 [0,1] 上定义了均匀分布。这就像一个 1 维空间。如果我们从中抽取一个大小为 1 的随机样本,并将其记作 X₁,那么我们有:
因此,X₁ 在区间边缘 0.05 个单位内的概率仅为 10%。现在假设我们有一个来自 100 维均匀分布的样本,记作 X₁, X₂, … X₁₀₀。这是一个 IID(独立同分布)样本,这意味着所有的 Xᵢ 相互独立且每个 Xᵢ 在区间 [0,1] 上具有均匀分布。因此,我们可以写出:
我们得到:
因此,至少有一个 Xᵢ 位于超立方体的某个面 0.05 单位以内的概率几乎为 1(这意味着该 Xᵢ 的观察值在该面 0.05 单位以内)。平均而言,在一个大小为 37594 的样本中,我们只能找到一个不在这个区域的数据点(1 / 2.66e-5 ≈ 37594)。这意味着我们需要一个非常大的样本才能得到仅有的几个不在超立方体面附近的数据点。
随着数据集中维度(或特征)的增加,为了准确概括机器学习模型所需的观察数量呈指数增长。维度诅咒使得训练数据集变得稀疏,模型预测的概括变得更加困难。因此,我们需要更多的训练数据(更多的观察)来概括模型。实际上,准备如此庞大的训练数据集可能不切实际。
图 7 显示了维度诅咒对随机采样影响的类比。最初,我们有一个二维射击目标和一个新手射手,他随机射击。射中最内圈的机会很高。在底部,我们有相同的目标,但现在具有维度诅咒。几乎所有的目标区域都属于最外圈,因此射中最内圈的机会几乎为零。
图 7
维度诅咒对随机采样的影响不仅限于均匀分布。实际上,无论其概率分布如何,这种现象都会发生在任何高维数据集中。让我们看看如果数据集中的特征具有正态分布会发生什么。假设我们数据集中的所有特征都是独立的,并且具有标准正态分布。因此,如果我们将这些特征组合成一个向量,该向量具有标准多元正态分布。图 8 显示了二维标准多元正态分布的 PDF。
图 8
标准正态分布的 PDF 是其边际分布 PDF 的乘积:
每个 f(xᵢ) 是标准正态分布的 PDF。使用这个方程,我们可以绘制标准正态分布在更高维度的 PDF。列表 3 绘制了 d 维标准正态分布在 xᵢ 轴上的 3 个 d 值的 PDF。请注意,PDF 具有对称形状,因此图形对于所有 xᵢ 和通过原点的所有线都是相同的。基于这些图,我们得出结论,对于任何 d 值,PDF 的最大值都在原点,且随着远离原点而下降。
# Listing 3
np.random.seed(0)
num_dims=[2, 5, 10]
xi = np.linspace(-3, 3, 500)
fig, axs = plt.subplots(1, 3, figsize=(19, 5))
plt.subplots_adjust(wspace=0.25)
for i, d in enumerate(num_dims):
pdf = norm.pdf(xi)**d
axs[i].plot(xi, pdf)
axs[i].set_xlabel('$x_i$', fontsize=28)
axs[i].set_xlim([-3, 3])
axs[i].set_title("d={}".format(d), fontsize=26)
axs[i].tick_params(axis='both', labelsize=12)
axs[0].set_ylabel('PDF', fontsize=26)
plt.show()
图 9
列表 4 从d-维标准多元正态分布中抽取 5000 个样本。然后,它计算该样本中每个d-维数据点到原点的欧几里得距离。最后,它创建这些距离的直方图。它使用了列表 1 中使用的相同d值。结果如图 10 所示。
# Listing 4
np.random.seed(0)
num_dims=[2,3,9, 15, 50, 100]
fig, axs = plt.subplots(2, 3, figsize=(19, 14))
plt.subplots_adjust(hspace=0.3)
for i, d in enumerate(num_dims):
samples = norm.rvs(size=5000*d).reshape(5000, d)
dist = np.linalg.norm(samples, axis=1)
axs[i//3, i%3].hist(dist, bins=25)
axs[i//3, i%3].set_title("d={}".format(d), fontsize=28)
axs[i//3, i%3].set_xlabel('Distance from origin', fontsize=22)
if i%3==0:
axs[i//3, i%3].set_ylabel('Number of data points', fontsize=22)
axs[i//3, i%3].set_xlim([0, 14])
axs[i//3, i%3].tick_params(axis='both', labelsize=16)
plt.show()
图 10
这里,直方图中每个区间的数据点数量与在该区间找到数据点的概率成正比。正如该图所示,即使在d=2 时,直方图的峰值也不在零处。在原点,PDF 具有最大值(图 8),然而,概率也依赖于我们进行积分的体积:
通过增加维度,径向直方图的峰值从原点处移动得更远。因此,大多数数据点都位于一个薄环中,几乎没有其他地方找到数据点的概率。请注意,在任何值的d下,PDF 的最大值仍然位于原点(图 9),然而,在原点附近获得数据点的概率几乎为零。同样,数据集并不代表它所采样的总体,我们没有来自非常接近原点且具有高 PDF 值的区域的数据点。
重要的是要注意,维度的诅咒与数据集的概率分布的 PDF 无关。例如,均匀分布在其支持上具有相同的值,无论我们有多少维度。然而,由于概率依赖于 PDF 和体积,它会受到维度数量的影响。
维度对距离函数的影响
维度对距离也有重要影响。首先,让我们看看“距离”是什么意思。设x为d维向量:
x的p-范数定义为:
我们可以使用p-范数计算向量x和y之间的距离:
请注意,还有其他类型的距离函数,但在本文中,我们只关注这种类型。当p=2 时,我们得到熟悉的欧几里得距离:
这是向量x-y的长度,也可以表示为||x-y|| (我们可以在p=2 时使用一个没有下标的p-范数)。现在,让我们深入探讨维度对距离函数的影响。假设我们数据集的维度是d。我们选择数据集中的一个点作为查询点,并用向量xq表示。我们找出离查询点最近和最远的数据点,并分别用向量xn和x_f表示(见图 11)。然后我们计算查询点与最近数据点和最远数据点之间的距离:
现在可以证明,在对数据分布进行某些合理假设的情况下,我们有:
因此,随着维度的增加,查询点到最远数据点的距离逐渐接近其到最近数据点的距离。因此,将最近的数据点与其他数据点区分开来变得不可能。
图 11
列表 5 显示了维度对距离函数的影响。它从定义在d-维单位超立方体上的均匀分布中抽取了 500 个样本。然后它计算了每对数据点之间的欧几里得距离,并绘制了这些距离的直方图。它使用了列表 1 中相同的d值。图 12 显示了这些直方图。请注意,在d-维单位超立方体中,两点之间的最小欧几里得距离为零,最大可能距离为:
因此,对于每个d值,直方图的x轴范围为 0 到√n。
# Listing 5
np.random.seed(0)
num_dims=[2,3,9, 15, 50, 500]
fig, axs = plt.subplots(2, 3, figsize=(19, 14))
plt.subplots_adjust(hspace=0.3)
for i, d in enumerate(num_dims):
samples = uniform.rvs(size=500*d).reshape(500, d)
dist_amtrix = distance_matrix(samples, samples)
dist_arr = dist_amtrix[np.triu_indices(len(dist_amtrix), k = 1)]
axs[i//3, i%3].hist(dist_arr, bins=30)
axs[i//3, i%3].set_title("d={}".format(d), fontsize=28)
axs[i//3, i%3].set_xlabel('Pairwise distance', fontsize=22)
if i%3==0:
axs[i//3, i%3].set_ylabel('Number of data points', fontsize=22)
axs[i//3, i%3].set_xlim([0, np.sqrt(d)])
axs[i//3, i%3].tick_params(axis='both', labelsize=16)
plt.show()
图 12
随着维度(d)的增加,直方图变得更加尖锐,因此所有成对距离都在一个狭窄范围内。这表明,对于数据集中的每个数据点,DMAX(到最远数据点的距离)接近 DMIN(到最近数据点的距离)。
让我解释一下这种效应背后的直觉。图 13 显示了一个查询点(xq)及其最近的(xn)和最远的数据点(x_f)。在二维空间中,每个向量
和
有两个分量。xn- xq 的分量用蓝色标记,xf- xq 的分量用红色标记。同样,在d-维空间中,这些向量有d个分量。
图 13
在二维空间中,从一个点到另一个点,我们应该通过 2 个维度。例如,在图 13 中,从xq到xf,我们应该向右移动l₁单位,向上移动l₂单位。请注意,l₁和l₂是向量xf-xq的组件,这些点之间的距离是l₁²+l₂²的平方根。同样,在d-维空间中,从xq到xf,我们应该通过d个维度。现在,向量xf-xq有d个组件。如果第i个组件用lᵢ表示,则意味着我们应该在第i个维度上移动lᵢ单位(图 13)。
列表 6 创建了一个从定义在d-维单位超立方体上的均匀分布中生成的大小为 500 的样本。然后,它随机选择一个查询数据点(xq),并找到其最近的(xn)和最远的(xf)数据点。接下来,它计算向量xn-xq和xf-x_q的组件。最后,这些组件的绝对值在d=2 和d=500 的两个条形图中绘制。结果显示在图 14 中。
# Listing 6
np.random.seed(9)
num_dims=[2,500]
fig, axs = plt.subplots(2, 2, figsize=(17, 11))
plt.subplots_adjust(hspace=0.3)
for i, d in enumerate(num_dims):
samples = uniform.rvs(size=500*d).reshape(500, d)
query = samples[0]
dist_amtrix = distance_matrix(samples, samples)
ind_min = np.argmin(dist_amtrix[0, 1:])+1
ind_max = np.argmax(dist_amtrix[0, 1:])+1
nearest = samples[ind_min]
farthest = samples[ind_max]
query_nearest_comps = np.abs(query - nearest)
query_farthest_comps = np.abs(query - farthest)
query_nearest_length = np.linalg.norm(query - nearest)
query_farthest_length = np.linalg.norm(query - farthest)
axs[0,i].bar(np.arange(1, d+1), query_nearest_comps,
color="blue", width=0.45, label="$x_n-x_q$")
axs[0,i].bar(np.arange(1, d+1), query_farthest_comps,
color="red", alpha=0.5, width=0.45, label="$x_f-x_q$")
axs[0,i].set_title("d={}".format(d), fontsize=26, pad=20)
axs[0,i].set_xlabel('Index of component', fontsize=18)
axs[0,i].set_ylabel('Abosulte value of component', fontsize=18)
if i==0:
axs[0,i].set_xticks(np.arange(1, d+1))
axs[0,1].set_ylim([0, 1.2])
axs[0,i].tick_params(axis='both', labelsize=16)
axs[0,i].legend(loc='best', fontsize=17)
axs[1,i].bar(["$||x_n-x_q||$", "$||x_f-x_q||$"],
[query_nearest_length, query_farthest_length],
width=0.2)
axs[1,i].tick_params(axis='both', labelsize=22)
axs[1,i].set_ylabel('Length', fontsize=18)
plt.show()
图 14
向量xn-xq的长度给出了xq和xn之间的距离。同样,||xf-xq||给出了xq和xf之间的距离。这些距离也在图 14 中的两个条形图中显示。正如图中所示,在二维空间中,向量xn-xq的组件的绝对值远小于xf-xq的组件。因此,||xf-xq||和||xn-xq||之间的差异是显著的。在 500 维空间中,我们有 500 个组件。正如你所见,xn-xq的某些组件的绝对值甚至大于xf-xq。请记住,向量的长度是从这些组件的平方和中得到的。因此,||xf-xq||和||xn-xq||现在相对接近。
我们知道所有数据点都是随机选择的,因此每个数据点的组件都是随机数。当维度很高时,从一个点到另一个点,我们应该通过每个维度移动一个随机距离(图 13),由于我们有很多维度,每对点之间的总距离大致相同。因此,从查询点到其他任何点的距离大致相同。事实上,随着维度的增加,点之间的平均距离增加,但这些距离之间的相对差异减少(图 14)。
图 15 给出了这一效果的类比。假设两个地方之间的路径由几个段组成。在一维空间中,我们只有一个段。在二维空间中,每对点之间有 2 个段,而在 d 维空间中,我们有 d 个段。当只有一个段时,路径长度给出了两个点之间的实际水平距离。因此,我们可以很容易地区分较近的点 (n) 和较远的点 (f)。
随着段数的增加,路径长度(即所有段长度的总和)增加,并与点之间的实际距离发生偏离。现在点之间的水平距离对路径的影响不大。相反,沿着段的垂直移动决定了路径长度。随着段数趋向于无穷大,q 和 n 之间的路径长度接近于 q 和 f 之间的路径长度。
图 15
一个具有 d 个段的路径类似于 d 维空间中两个点之间的距离,每个段可以表示连接这些点的向量的一个分量。随着维度的增加,我们有更多的段(分量),距离之间的相对差异接近于零。
维度对学习模型的影响
如前一节所述,在高维空间中,接近度或距离的概念不再有意义。因此,任何依赖距离函数的机器学习模型在高维空间中可能会崩溃。换句话说,当训练数据集中有很多特征时,这些模型将无法给出可靠的预测。一个例子是 k-NN (k 最近邻) 算法。在这一节中,我们将探讨维度诅咒如何影响 k-NN 模型的预测误差(这些例子是 [1] 中的修改版本)。
我们的数据集有 d 个特征和 1000 个示例(观察)。这些示例来自单位 d 维超立方体上的均匀分布。因此,这个数据集的每个示例可以写作:
这个观察的目标定义为:
因此,目标仅仅是每个示例第一个特征的平方。我们使用一个 k-NN 模型,通过这个训练数据集预测测试数据点的目标。为了预测测试数据点的目标,k-NN 模型首先在训练数据集中找到该数据点的 k 个最近邻(k 个离测试点最近的数据点)。它使用距离函数(在这个例子中是欧几里得距离)来找到它们。测试点的预测目标是这 k 个最近邻的目标值的平均值。在这个例子中,我们使用 3 个最近邻(k=3)。
测试点位于超立方体的中心:
我们进行了 1000 次模拟。在每次模拟中,我们从 d 维均匀分布中抽取 1000 个数据点以创建训练数据集的示例,并计算它们的目标值。然后,我们在该训练数据集上训练一个 k-NN 模型(k=3)。最后,我们使用该模型预测测试数据点的目标(记作 y^_t),并与实际目标 0.25 比较。一旦我们获得所有模拟的预测目标,就可以计算这些预测的偏差、方差和均方误差(MSE)。偏差定义为:
这是在这些模拟中预测目标(y^t)的均值与测试点的实际目标(yt)之间的差异。方差是这些模拟中预测目标的方差:
均方误差(MSE)定义为:
可以证明:
列表 7 计算了该示例在不同 d 值下的偏差、方差和 MSE。结果绘制在图 16 中。
# Listing 7
np.random.seed(2)
num_dims = np.arange(1, 11)
num_simulations = 1000
var_list = []
bias_list = []
for d in num_dims:
pred_list = []
for i in range(num_simulations):
X = uniform.rvs(size=1000*d).reshape(1000, d)
y = X[:,0]**2
knn_model = KNeighborsRegressor(n_neighbors=3)
knn_model.fit(X, y)
pred_list.append(knn_model.predict(0.5*np.ones((1,d))))
var_list.append(np.var(pred_list))
bias_list.append((np.mean(pred_list)-0.25)**2)
error = np.array(bias_list) + np.array(var_list)
plt.plot(num_dims, var_list, 'o-', label="$Variance$")
plt.plot(num_dims, bias_list, 'o-', label="$Bias²$")
plt.plot(num_dims, error, 'o-', label="$MSE$")
plt.xlabel("d", fontsize=16)
plt.ylabel("$MSE, Bias², Variance$", fontsize=16)
plt.legend(loc='best', fontsize=14)
plt.xticks(num_dims)
plt.show()
图 16
k-NN 模型假设相近的数据点也应具有相近的目标。然而,在高维数据集中,所有数据点之间的距离大致相同,因此 k 个最近邻可能距离测试点非常远。因此,这些邻居的平均目标不再是测试点实际目标的准确预测。
列表 8 显示了 d=2 和 d=10 的所有模拟中 k-NN 预测(y^_t)的比较(图 17)。它还用黑色虚线显示了测试点的实际目标(y_t),用蓝线显示了所有 k-NN 预测的均值。请注意,黑色虚线和蓝线之间的距离表示偏差。
# Listing 8
np.random.seed(2)
num_dims = [2, 10]
num_simulations = 1000
fig, axs = plt.subplots(1, 2, figsize=(10, 5))
plt.subplots_adjust(wspace=0.3)
for i, d in enumerate(num_dims):
pred_list = []
for j in range(num_simulations):
X = uniform.rvs(size=1000*d).reshape(1000, d)
y = X[:,0]**2
knn_model = KNeighborsRegressor(n_neighbors=3)
knn_model.fit(X, y)
pred_list.append(knn_model.predict(0.5*np.ones((1,d))))
axs[i].scatter([0]*num_simulations, pred_list,
color="blue", alpha=0.2, label="$\hat{y}_t$")
axs[i].axhline(y = 0.25, color = 'black',
linestyle='--', label="$y_t$", linewidth=3)
axs[i].axhline(y = np.mean(pred_list),
color = 'blue', label="$E[\hat{y}_t]$")
axs[i].set_ylim([0, 0.7])
axs[i].set_title("d={}".format(d), fontsize=18)
axs[i].set_ylabel('$\hat{y}_t$', fontsize=18)
axs[i].legend(loc='best', fontsize=14)
axs[i].set_xticks([])
plt.show()
图 17
随着维度的增加,最近邻会接近超立方体的面,并且距离超立方体中心的测试点非常远。这些点的 x₁ 组件不一定接近测试点的 x₁,并且可能变化很大,因此预测的方差随着 d 的增加迅速增加。然而,平均来说,它们接近测试点的 x₁,所以偏差保持相对较小(图 16)。均方误差(MSE)与偏差和方差成正比,因此它随维度增加而增加。
列表 9 显示了另一个示例。这里的设置类似于列表 7,但数据集的目标定义为:
其中
测试点再次位于超立方体的中心:
这个例子的偏差、方差和 MSE 的图示如图 18 所示。在这里,偏差随着维度的增加迅速增加,而方差保持相对较小。
# Listing 9
np.random.seed(2)
num_dims = np.arange(1,11)
num_simulations = 1000
var_list = []
bias_list = []
for d in num_dims:
pred_list = []
for i in range(num_simulations):
X = uniform.rvs(size=1000*d).reshape(1000, d)
y = np.exp(-5*np.linalg.norm(X-0.5*np.ones((1,d)), axis=1)**2)
knn_model = KNeighborsRegressor(n_neighbors=3)
knn_model.fit(X, y)
pred_list.append(knn_model.predict(0.5*np.ones((1,d))))
var_list.append(np.var(pred_list))
bias_list.append((np.mean(pred_list)-1)**2)
error = np.array(bias_list) + np.array(var_list)
plt.plot(num_dims, var_list, 'o-', label="$Variance$")
plt.plot(num_dims, bias_list, 'o-', label="$Bias²$")
plt.plot(num_dims, error, 'o-', label="$MSE$")
plt.xlabel("d", fontsize=16)
plt.ylabel("$MSE, Bias², Variance$", fontsize=16)
plt.legend(loc='best', fontsize=14)
plt.xticks(num_dims)
plt.show()
图 18
记住,随着维度的增加,每对点之间的距离增加,但这些距离之间的相对差异接近于零。因此,包括最近邻在内的所有数据点,||x|| 增加,而 y 变为零。因此,最近邻的平均 y^ 与测试点的目标偏离。结果是偏差和 MSE 增加,但方差保持较小。
总结来说,维度的增加会导致均方误差(MSE)增加。然而,它对偏差或方差的影响取决于数据集目标的定义。这些例子清楚地表明,k-最近邻(k-NN)对于高维数据并不是一个可靠的模型。
在本文中,我们讨论了维度诅咒及其对体积和距离函数的影响。我们看到,在高维空间中,几乎所有数据点都将远离原点。因此,如果我们使用高维数据集来训练模型,那么对于接近原点的数据点,模型的预测将不可靠。此外,距离的概念也不再有意义。在高维数据集中,所有数据点之间的成对距离非常接近。因此,依赖于距离函数的机器学习模型将无法为高维数据集提供准确的预测。
参考文献
[1] Hastie, T., et al. The Elements of Statistical Learning. Springer, 2009.
讨厌数学的人的数据分析师学习路线图
实际上,在数据分析这个非常实际的领域中,数学是相当有趣的。
·
关注 发表在 Towards Data Science ·7 分钟阅读·2023 年 3 月 7 日
--
图片由 Nabit Photos 提供,来源于 Unsplash
在科技行业中,数学和数据分析几乎是同义词。这也是为什么很多人会对从事分析工作感到恐惧,因为存在一种先入为主的观念,认为数学就是这份工作的全部。
事实上,虽然我不能说完全没有数学涉及,但我可以告诉你,你做的数学实际上是相当有趣的。没有数据科学家常见的那些理论或虚数的恐怖——相反,你会使用实际、易于应用的数学来解释发生了什么,甚至对可能发生的事情做出简单的预测。
你甚至可能已经掌握了成为数据分析师所需的所有数学知识。事实上,我整理了一个 15 分钟的阅读材料,涵盖了你在本科统计课程中会学到的所有内容来帮助你入门。就是这么简单。
作为一个曾经在高中数学课程中失败过的人,我可以毫无保留地告诉你,这个为讨厌数学的人设计的数据分析学习路线图正是你所需要的——不多也不少——帮助你迈出数据分析领域的第一步。你只需要对学习一点点数学保持开放的心态,剩下的就会很容易。你可能会花更多的时间学习编码和如何进行数据分析,而不是学习所有你需要的数学知识。
这条路线图涵盖了你需要掌握的所有学习方面,并且提供了一个最低限度的数学计划,帮助你在工作中获得成功。
第一步:学习 Excel、SQL 和 Python
作者的说明:在我之前创建的路线图中,我总是建议希望成为数据分析师的人学习 Python 或 R。我之所以改变这个建议,是因为学习 R 的困难(而且 Python 是业界标准)。如果你将 Python 作为你唯一的编程语言开始学习,你将拥有足够的功能来拥有一个长久而快乐的数据分析师职业生涯。
Python 是你刚刚开始数据分析时可以选择的最简单的编程语言。它功能强大,拥有众多功能,而且如果你决定这个领域不适合你,它在数据分析之外也有许多应用。它极其宽容,几乎字面上地给你你所要求的(这使你善于提出正确的问题),并且允许你进行有效的分析,而无需成为编程细节方面的专家。
我对学习 Python 的最佳建议是练习 Kaggle 或 Leetcode 上的练习题。尽管这种重复看起来可能有些无聊,但这是测试自己并强迫大脑适应编程问题模式的最快方法。
但首先,在你开始学习 Python 之前,你需要先学习 Excel。为什么?因为 Excel 可能是你成为数据分析师所需的唯一工具。
Excel(尽管对一些人来说显得有些过时)仍然是一个非常强大的工具,你可以用它来进行大多数简单的数据分析,包括公司数据的汇总以及趋势的建立和简单预测。我们都说我们会“真正”学习 Excel,这就是你真正学习 Excel 内部运作的信号,因为这个技能可能让你因为这项技能而在小型企业中找到你的第一份入门级数据分析师职位。
我最喜欢学习 Excel 中众多技巧的方法之一是寻找 TikTok 上分享 Excel 技巧的视频。有数百个简短的视频可以帮助你快速学习程序中内置的众多快捷键,从而加快工作流程,并通过这种形式的微学习让你更熟悉程序。
最后,你将学习 SQL,这是一种允许你在数据库中操作数据的语言。SQL 可能是一种复杂的语言,因此我最好的建议之一是随着学习的进展为自己创建一个备忘单。这个备忘单应该展示从 SQL 命令的正确语法到你可以创建的不同类型的数据连接。
你不必记住每一个函数,但应该对像 COUNT、CONCAT、TRIM、MAX/MIN、GETDATE 和 CONVERT 等函数有所了解。记住函数是浪费时间的。如我之前所提,制作一个备忘单,突出显示如何使用一些你最常用的函数,并学会如何在 Google 上搜索正确的问题来找到其他函数。
我快速熟悉 SQL 的最佳方法之一是下载一个免费的数据库,然后在其中尝试所有的函数和语法。拥有一个可以随意操作的数据库,而不必担心出错或破坏数据库,是让你深入了解代码的好方法(在我看来是学习 SQL 的最佳方式)。
第 2 步:刷新你的代数和统计记忆
如果你能做代数和统计,你基本上已经拥有了成为数据分析师所需的一切。
事实上,如果你阅读我概述的一个 15 分钟的本科统计课程,你已经基本上度过了最困难的部分。看见了吗?我说这是一个适合讨厌数学的人的数据分析师学习路线图。
[## 初级统计学课程介绍 - towardsdatascience.com
事实是,作为数据分析师你总是可以学到更多的数学知识。你永远无法掌握足够的数学。然而,如果你真的厌恶数学,你可以只学代数和统计学。
要掌握基础知识,我建议你浏览可汗学院的代数 1、代数 2 和 AP 统计学讲座系列。
代数 1 课程,通常在 9 年级教授,涵盖线性方程、不等式、函数和图形等内容…
代数 2 课程,通常在 11 年级教授,涵盖多项式;复数;有理指数等内容…
如果你看到此消息,说明我们在加载网站外部资源时遇到问题。如果你…
如果你想挑战自己,可汗学院的线性代数系列是扩展技能的绝佳方式,莱昂纳德教授在 Youtube 上的微积分 1 讲座系列也是如此。
如果你看到此消息,说明我们在加载网站外部资源时遇到问题。如果你…
所有这些资源以相对轻松的方式分享数学知识,使你能快速完成成为数据分析师的数学学习部分,直接进入有趣的内容:数据分析和可视化。
第三步:学习数据分析和可视化
现在是时候把所有内容结合起来,分析一些数据。这涉及学习如何提出正确的问题、收集所需的数据、清理数据、分析数据以获得洞察和答案,并通过可视化解释数据,使任何人都能轻松理解。
为了更好地学习数据分析和可视化,我相信花更少的时间在在线课程上,而更多时间在实际操作上。例如,上面我链接的视频长达 4 小时。这是你在创建自己的数据分析之前应花费的所有学习时间。你在自己做项目时会学到比坐在电脑前跟着视频学习更多的数据分析知识。
根据经验,这是你学习如何分析数据所需观看的唯一视频——在此之外几乎没有更多需要学习的内容。你可以通过学习如何使用特定的可视化工具来扩展你的知识,或者了解如何使你的统计数据更准确,但这些属于专业发展领域——而不是核心学习。
最后的想法
看?这并没有那么痛苦(即:数学很多)。
实际上,数学在学习过程中的作用可能是最小的,尤其是当你已经知道一些数学知识时。成为数据分析师的最好部分是你不真的需要知道数学是如何工作的(就像数据科学家一样)——你只需要知道它有效,并知道何时使用它。
当我自学数据分析时,我发现自己在学习数学上的时间最少。大部分时间都花在学习如何编程和做数据分析项目上,以便细化我的技能。因此,不要让你对数学的厌恶让你远离这个其实只是整个工作小小一部分的领域。当你意识到你使用的数学是非常实用的,你会学会享受用它来揭示细节和回答问题。只要你能够在实际应用中处理一些数字,喜欢挑战,并且能用数据讲好故事,你就可以毫无问题地成为数据分析师。
订阅以便直接将我的故事发送到你的邮箱:故事订阅
请成为会员以通过我的推荐链接获得对 Medium 的无限访问(这不会额外增加你的费用,我会获得小额佣金):Medium 会员
支持我的写作,捐赠以资助更多类似的故事创作:捐赠
“Segment Anything”中的以数据为中心的人工智能概念
原文:
towardsdatascience.com/the-data-centric-ai-concepts-in-segment-anything-8eea556ac9d
解读“Segment Anything”中使用的以数据为中心的人工智能概念,这是第一个用于图像分割的基础模型
·发布于 Towards Data Science ·阅读时长 7 分钟·2023 年 5 月 31 日
--
Segment Anything 数据集构建。图片来源于论文 arxiv.org/pdf/2304.02643.pdf
人工智能 (AI) 已经取得了显著进展,特别是在开发基础模型方面,这些模型经过大量数据的训练,能够适应广泛的下游任务。
基础模型的一个显著成功是 大语言模型 (LLMs)。这些模型可以高精度地执行复杂任务,如语言翻译、文本摘要和问答。
基础模型也开始改变计算机视觉领域的游戏规则。Meta 的“Segment Anything”是最近出现的一个引起轰动的发展。
“Segment Anything”的成功可以归因于其庞大的标注数据集,这在实现其卓越性能方面发挥了至关重要的作用。如 Segment Anything 论文 中所述,模型架构出乎意料地简单且轻量。
在这篇文章中,借鉴我们最近的调查论文 [1,2] 中的见解,我们将通过 以数据为中心的人工智能 的视角更深入地探讨“Segment Anything”。
“Segment Anything”可以做什么?
简而言之,图像分割任务是预测一个掩码来分离图像中的感兴趣区域,例如物体、人物等。分割是计算机视觉中非常重要的任务,使图像更有意义,更易于分析。
Segment Anything 和其他图像分割方法的区别在于引入了提示来指定分割位置。提示可以是模糊的,例如一个点,一个框等。
这张图片是从 segment-anything.com/
截取的,图片由作者上传。
什么是数据中心的人工智能?
数据中心人工智能与模型中心人工智能的比较。 arxiv.org/abs/2301.04819
图片由作者提供。
数据中心的人工智能是一种新颖的人工智能系统开发方法,近年来受到关注,并由人工智能先驱安德鲁·吴推广。
数据中心的人工智能是系统性地工程化用于构建人工智能系统的数据的学科。 — 安德鲁·吴
之前,我们的主要关注点是使用变化不大的数据来开发更好的模型,这被称为模型中心的人工智能。然而,这种方法在现实世界场景中可能存在问题,因为它未能考虑数据中可能出现的问题,包括不准确的标签、重复数据和偏见。因此,过拟合数据集可能不会导致模型行为的改善。
另一方面,数据中心的人工智能优先考虑提高用于创建人工智能系统的数据的质量和数量。重点放在数据本身,相对固定的模型。采用数据中心方法开发人工智能系统在现实世界应用中更具前景,因为模型的最大能力是由用于训练的数据决定的。
区分“数据中心”和“数据驱动”方法至关重要。“数据驱动”方法仅依赖于数据来引导人工智能开发,但重点仍然在于创建模型,而不是工程化数据,因此与“数据中心”方法根本不同。
数据中心人工智能框架 包括三个主要目标:
-
训练数据开发 涉及收集和生成高质量、多样化的数据,以促进机器学习模型的训练。
-
推理数据开发 涉及构建创新的评估集,通过工程化的数据输入(如提示工程)为模型提供详细的洞察,或解锁模型的特定能力。
-
数据维护 旨在确保在不断变化的环境中数据的质量和可靠性。
数据中心人工智能框架。 arxiv.org/abs/2303.10158
。图片由作者提供。
在 Segment Anything 中使用的模型
Segment Anything 模型。 图片来自论文 arxiv.org/pdf/2304.02643.pdf
模型设计令人惊讶地简单。该模型主要由三部分组成:
-
提示编码器: 这一部分用于获取提示的表示,可以通过位置编码或卷积来实现。
-
图像编码器: 这一部分直接使用了 Vision Transformer (ViT),没有任何特殊修改。
-
轻量级掩码解码器: 这一部分主要融合了提示嵌入和图像嵌入,使用了如注意力机制等方法。之所以称其为轻量级,是因为它只有少量的层。
轻量级掩码解码器非常有趣,因为它允许模型即使仅使用 CPU 也能轻松部署。以下是 Segment Anything 的作者提供的评论。
令人惊讶的是,我们发现一个简单的设计满足了所有三个约束条件:一个强大的图像编码器计算图像嵌入,一个提示编码器嵌入提示,然后这两个信息源在一个轻量级掩码解码器中结合,以预测分割掩码。
因此,Segment Anything 强大性能的秘密很可能不在于模型设计,因为它非常简单且轻量化。
Segment Anything 中的数据驱动 AI 概念
训练 Segment Anything 的核心在于一个包含超过十亿个掩码的大型标注数据集,这比现有的分割数据集大了 400 倍。他们是如何做到的呢?作者们使用了一个数据引擎来执行标注,这可以大致分为三个步骤:
-
辅助手动标注: 这一步可以理解为一种主动学习过程。首先,在公共数据集上训练初始模型。接下来,标注者修改预测的掩码。最后,使用新标注的数据训练模型。这三个步骤重复了六次,最终生成了 430 万条掩码标注。
-
半自动标注: 这一步的目标是增加掩码的多样性,这也可以理解为一种主动学习过程。简单来说,如果模型能够自动生成良好的掩码,那么人工标注者就不需要再标注这些掩码,人工可以集中在模型不够自信的掩码上。用来找出自信掩码的方法非常有趣,涉及对第一步掩码进行目标检测。例如,假设图像中有 20 个可能的掩码。我们首先使用当前模型进行分割,但这可能只会标注出部分掩码,有些掩码标注得不够好。现在我们需要自动识别哪些掩码是好的(自信的)。本文的方法是对预测的掩码进行目标检测,看看是否能够在图像中检测到物体。如果检测到物体,我们就认为对应的掩码是自信的。假设这个过程识别出八个自信的掩码,那么标注者就可以标注剩下的 12 个,从而节省了人工努力。上述过程重复了五次,增加了另外 590 万条掩码标注。
-
全自动标注: 简单来说,这一步使用在前一步中训练的模型来标注数据。为了提高标注质量,采用了一些策略,包括:
(1) 根据预测的交并比(IoU)值过滤掉不太自信的掩码(模型有一个头部用于预测 IoU)。
(2) 仅考虑稳定的掩码,即如果阈值稍微调高或调低 0.5,掩码基本保持不变。具体来说,对于每个像素,模型输出一个介于 0 和 1 之间的值。我们通常使用 0.5 作为阈值来决定一个像素是否被掩码。稳定性意味着,当阈值调整到接近 0.5 的某个程度(例如 0.45 到 0.55)时,相应的掩码基本保持不变,这表明模型的预测在边界的两侧有显著不同。
(3) 进行了去重,使用了非极大值抑制(NMS)。
这一步标注了 110 亿个掩码(数量增加了 100 倍以上)。
这个过程听起来是否很熟悉?没错,用于 ChatGPT 的人类反馈强化学习(RLHF)与上述过程非常相似。这两种方法的共同点在于,模型不是直接依赖人类来标注数据,而是首先通过人类输入对模型进行训练,然后再使用模型来标注数据。在 RLHF 中,训练一个奖励模型来为强化学习提供奖励,而在 Segment Anything 中,模型则是针对直接图像标注进行训练的。
总结
Segment Anything 的核心贡献在于其大量的标注数据,这显示了数据中心人工智能概念的关键重要性。基础模型在计算机视觉领域的成功可以被视为一个必然事件,但令人惊讶的是,这一切发生得如此之快。展望未来,我相信其他 AI 子领域,甚至非 AI 和非计算机相关领域,也会在适当的时候出现基础模型。
无论技术如何演变,提高数据的质量和数量始终是提升 AI 性能的有效途径,使得数据中心 AI 概念愈加重要。
我希望这篇文章能为你的工作提供一些灵感。你可以通过以下论文/资源了解更多关于数据中心 AI 框架的信息:
如果你觉得这篇文章有趣,你可能还会想查看我之前的文章:GPT 模型背后的数据中心 AI 概念是什么?
敬请关注!
数据科学家的工具箱:利用 scikit-learn 的顶级功能取得成功
理解 scikit-learn:统一的机器学习方法
·发表于 Towards Data Science ·12 min read·2023 年 6 月 12 日
--
图片来源:Tayeb MEZAHDIA 在 Pixabay
Python 有很多库,使其成为最常用的编程语言之一。它们大多数具有类似的功能,可以相互使用并达到相同的结果。但当涉及到机器学习时,我们唯一可以谈论的库就是sklearn
。
无论你是 ML 从业者还是初学者,我知道你对sklearn
有一定的了解。但有时我们使用某些工具只是因为大家都在用,而没有提出任何问题。
比如,你是否曾经问过自己:“sklearn
中最重要的 5 个特性是什么?” 好吧,让我告诉你一件事:我大约花了 2 年的时间才问自己这个问题,所以如果你还没有问,也不必感到羞愧。
所以,在这篇文章中,我将回答这个问题;以下是你将在这里找到的内容:
**Table of contents:**
Feature #1: consistency
Feature #2: wide range of algorithms
Feature #3: data preprocessing and feature engineering
Feature #4: model evaluation and validation
Feature #5: integration with the Python data science ecosystem
Coding examples
特性 #1:一致性
sklearn
的强大之处在于它是一致的库。这是什么意思?这意味着它具有:
-
轻松与外部 DL/ML 库集成。它可以与外部 ML/DL 库集成,例如
Keras
、Tensorflow
和XGBoost
,仅举几个例子。这种兼容性使我们能够将sklearn
的功能与这些库的功能结合起来,使得我们数据专业人员的开发阶段变得轻松无痛。 -
使用简便性。
sklearn
在其提供的各种机器学习算法中保持统一的接口。这样我们不需要学习新的语法或模式:我们只需遵守“相同的规则”并思考开发我们的机器学习算法。你能想象在处理不同算法时需要处理不同的语法吗?我可不想这样(对不起,如果我不能!)。 -
文档完善。
sklearn
提供了详尽的文档,包括教程和使用示例。文档还涵盖了库的各个方面,从数据预处理到模型评估。你可以在这里参考。
特征 #2:广泛的算法范围
sklearn
库提供了非常广泛的机器学习算法,从监督学习到无监督学习,包括回归和分类模型。
我们作为数据科学家所拥有的这种广泛可能性,使得 sklearn
成为机器学习的完美库:一个适用于我们日常工作的所有模型的环境。
我们只需要选择最适合我们特定问题领域的算法,而不必抱怨集成和其他类似问题:这是一个巨大的优势!
特征 #3:数据预处理和特征工程
数据预处理和特征工程是机器学习问题的重要部分,因为它们涉及为机器学习算法准备数据。以下是 sklearn
提供的用于这些任务的特征:
数据预处理
-
处理缺失数据。如果你练习机器学习一段时间,我相信你会发现处理缺失数据有些痛苦。或者,至少对我来说是这样。
sklearn
提供了不同的处理缺失数据的特征,例如:a)SimpleImputer()
类,它允许我们使用不同的策略(如均值、中位数、众数或常数值)来填补缺失值。填补可以应用于特定列或整个数据集。b)IterativeImputer()
类,它利用机器学习模型迭代填补缺失值。它使用选定模型的预测来填补缺失值。c)MissingIndicator()
类,用于创建数据集中缺失值的二进制指示器。它添加了一个新特征,标记值是否原本缺失,为模型提供额外的信息。 -
特征缩放。具有不同尺度的特征可能会以不良方式影响许多机器学习算法的性能。幸运的是,
sklearn
提供了一些缩放特征的方法,例如:a) 标准化(StandardScaler()
),它将特征缩放到均值为零、方差为一的状态,b) 归一化(MinMaxScaler()
),它将特征缩放到特定范围内。 -
编码分类变量。许多机器学习算法无法直接处理分类变量。因此,
sklearn
提供了几种编码技术,例如:a)独热编码(OneHotEncoder()
)和 b)有序编码(OrdinalEncoder()
),将分类变量转换为数值表示。这使得处理字符串形式的数据并将其转换为数字变得简单,以便我们可以将其输入到机器学习算法中。 -
处理异常值。异常值对数据科学家来说是一个普遍的难题,因为它们会影响机器学习模型的性能,因此需要特别处理。
sklearn
提供了一些异常值检测和去除的技术,例如局部离群因子(LOF()
)算法和孤立森林算法(IsolationForest()
)。注意,这些都是无监督的机器学习算法。
特征工程:
-
特征提取。
sklearn
提供了不同的方法来从数据中提取有意义的特征。实际上,并不是我们拥有的所有特征对于机器学习问题都是有意义的,通常我们只处理有意义的特征是个普遍的经验法则。在特征提取方面,sklearn
提供了:a)CountVectorizer()
类,它可以将文本文档转换为令牌计数矩阵;b)TfidfVectorizer()
类,它计算词频-逆文档频率(TF-IDF)来表示文本数据。 -
降维。
sklearn
提供了各种降维技术,这些技术对于减少特征数量同时保留关键信息非常有用。两种流行的方法是:a)主成分分析(PCA()
)和 b)t-分布随机邻居嵌入(TSNE()
)。 -
特征选择。最后,
sklearn
还提供了多种特征选择方法,例如:a)单变量特征选择(SelectKBest()
),b)递归特征消除(RFE()
),和 c)基于树模型的特征重要性排名(SelectFromModel()
)。
特征 #4:模型评估和验证
模型评估和验证是机器学习中至关重要的活动。实际上,作为数据科学家,我们的角色是找到最适合数据的模型;但这到底意味着什么呢?
这意味着我们需要训练一些机器学习模型,并找到那些在未见数据上做出最佳预测(或分类)的模型。这意味着我们首先需要在训练数据上评估一些机器学习模型,然后在新的、未见的数据上验证它们。sklearn
提供了不同的功能来帮助我们完成这些基本任务。
-
交叉验证技术。交叉验证是一种重要的模型评估技术,帮助我们应对过拟合。它通过将数据划分为多个子集,在这些子集的不同组合上训练模型,并平均结果来估计模型的性能。
sklearn
为我们提供了以下交叉验证方法:a)k 折交叉验证,b)分层 k 折交叉验证,以及 c)留一交叉验证。 -
评估指标。
sklearn
提供了广泛的评估指标,让我们能够数值化分类和回归模型的性能。对于分类任务,我们可以提到,如accuracy
、precision
、recall
和 AUC/ROC。回归任务则可以使用均方误差(MSE
)、均绝对误差(MAE
)、R² 等指标进行评估。如果你需要深入了解如何使用这些指标(以及sklearn
提供的所有主要指标),这里有 分类指标的终极指南。这里还有 回归任务的终极指南。 -
超参数调优。模型评估和验证与超参数调优密切相关,后者涉及选择最佳的模型参数组合以获得 ML 算法的最佳性能。
sklearn
提供了像GridSearchCV
和RandomizedSearchCV
这样的工具用于使用交叉验证进行超参数调优。这些技术系统地探索超参数空间,识别出最佳的超参数组合,从而获得最佳模型性能,基于所选择的评估指标。
特性 #5:与 Python 数据科学生态系统的集成
sklearn
的超能力之一是与 Python 数据科学生态系统的集成。这使得数据科学家能够利用 sklearn
的力量与数据处理、可视化和分析的库和工具结合。以下是一些例子:
-
Pandas 和 Numpy。
sklearn
与Pandas
数据框集成的可能性使用户能够将数据框直接传递给sklearn
的函数和类。这使得在将数据输入sklearn
模型之前,使用Pandas
进行数据预处理和清理变得非常方便。sklearn
也在数据表示上严重依赖NumPy
数组。实际上,它可以直接接受NumPy
数组作为训练任务的输入。 -
Matplotlib 和 Seaborn。
sklearn
提供了内置支持,用于使用Matplotlib
绘制指标和模型评估结果。这使用户能够可视化模型性能、特征重要性和其他相关统计数据,帮助我们理解我们的 ML 模型。此外,由于Seaborn
是一个建立在Matplotlib
之上的数据可视化库,sklearn
也与之兼容,使用户能够将sklearn
的 ML 算法的力量与Seaborn
的美观图表相结合,用于探索性数据分析和模型评估。
代码示例
在最后一段中,我想强调一些使用示例。如果你有使用 sklearn
的经验,我知道你知道如何使用它。但请考虑到,这里重点不是展示这些特性如何工作,而是强调工作流程的运作方式,基于我们在前面段落中所说的内容。因为,通常我们使用一些工具只是为了使用它们,且因为每个人都这样做,而不提出任何问题。
示例 #1:与 Pandas 和 NumPy 的集成
让我们看看如何与 sklearn
、Pandas
和 NumPy
集成。为了简单起见,我们可以使用 ChatGPT 的提示。但在此之前,你可能需要了解如何作为数据科学家有效地使用提示工程:
如果你从事数据工作,如何以及为什么编写有效的提示
[towardsdatascience.com
提示可能是以下内容:“创建一个用于回归分析的 Pandas 数据框,并包含 5 个特征。然后,对数据进行归一化,并将其分为训练集和测试集。使用线性回归模型拟合测试集。打印训练集和测试集的 R² 值”。
然后我们得到:
当然!我将创建一个包含五个特征的示例 Pandas 数据框,对数据进行归一化,将其分为训练集和测试集,拟合一个线性回归模型,并打印训练集和测试集的 R² 分数。这是一个示例实现:
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LinearRegression
from sklearn.metrics import r2_score
from sklearn.preprocessing import StandardScaler
# Generating random features with at least 50 occurrences each
np.random.seed(42)
feature1 = np.random.randint(0, 10, size=100)
feature2 = np.random.randint(0, 10, size=100)
feature3 = np.random.randint(0, 10, size=100)
feature4 = np.random.randint(0, 10, size=100)
feature5 = np.random.randint(0, 10, size=100)
target = np.random.randint(0, 100, size=100)
# Creating a Pandas data frame
data = {
'Feature1': feature1,
'Feature2': feature2,
'Feature3': feature3,
'Feature4': feature4,
'Feature5': feature5,
'Target': target
}
df = pd.DataFrame(data)
# Splitting the data into features and target
X = df.drop('Target', axis=1)
y = df['Target']
# Normalizing the data using StandardScaler
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
# Splitting the normalized data into train and test sets
X_train, X_test, y_train, y_test = train_test_split(X_scaled, y, test_size=0.2, random_state=42)
# Fitting the linear regression model
reg_model = LinearRegression()
reg_model.fit(X_train, y_train)
# Predicting the target variable for both train and test sets
y_train_pred = reg_model.predict(X_train)
y_test_pred = reg_model.predict(X_test)
# Calculating R² scores for train and test sets
r2_train = r2_score(y_train, y_train_pred)
r2_test = r2_score(y_test, y_test_pred)
# Printing the R² scores
print("R² score for train set:", r2_train)
print("R² score for test set:", r2_test)
在这个例子中,我创建了一个包含五个特征(
Feature1
到Feature5
)和一个目标变量(Target
)的 Pandas 数据框。然后,我使用 scikit-learn 的StandardScaler
对特征数据进行了归一化。之后,我将归一化的数据按 80:20 的比例分为训练集和测试集。接着,我在训练集上训练了一个线性回归模型,并预测了训练集和测试集的目标变量。最后,我计算并打印了训练集和测试集的 R² 分数。
所以,这里是在工作流程层面发生的事情:
-
使用
NumPy
创建特征和标签。 -
特征和标签成为了一个
Pandas
数据框。 -
Pandas
数据框变成了X
和y
:特征的数据框和标签的Pandas
系列。 -
X
和y
已被缩放和拆分。然后,线性回归模型进行预测,并使用 R²指标在训练集和测试集上进行评估。
所以,如我们所见,我们有一个从NumPy
开始,到sklearn
结束的直接工作流,给出所需的结果。不需要任何“奇怪的操作”或集成。
示例 #2:与 Matplotlib 的集成
为了展示sklearn
与Matplotlib
之间的集成,我们再次利用 ChatGPT,使用以下提示:“使用 NumPy 生成的数据在 Python 中创建一个二分类问题。标准化并将数据拆分为训练集和测试集。使用随机森林和 KNN 分类器拟合训练集。打印训练集的 ROC 曲线。”
我们得到:
当然!我们将使用 Python 生成随机数据来创建一个二分类问题,然后我们将标准化并将数据拆分为训练集和测试集。我们将使用随机森林和 K 近邻(KNN)分类器拟合训练集,最后,我们将绘制训练集的 ROC 曲线。
这是代码:
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.neighbors import KNeighborsClassifier
from sklearn.metrics import roc_curve, roc_auc_score
import matplotlib.pyplot as plt
# Set a random seed for reproducibility
np.random.seed(42)
# Generate random data
num_samples = 1000
num_features = 10
# Generate features (X)
X = np.random.randn(num_samples, num_features)
# Generate labels (y)
y = np.random.randint(2, size=num_samples)
# Standardize the features
X = (X - np.mean(X, axis=0)) / np.std(X, axis=0)
# Split the data into train and test sets
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
# Fit the train set using random forest classifier
rf_clf = RandomForestClassifier()
rf_clf.fit(X_train, y_train)
# Fit the train set using K-nearest neighbors (KNN) classifier
knn_clf = KNeighborsClassifier()
knn_clf.fit(X_train, y_train)
# Predict probabilities for the positive class (class 1)
y_train_rf_probs = rf_clf.predict_proba(X_train)[:, 1]
y_train_knn_probs = knn_clf.predict_proba(X_train)[:, 1]
# Calculate the false positive rate (FPR), true positive rate (TPR), and thresholds for the ROC curve
rf_fpr, rf_tpr, rf_thresholds = roc_curve(y_train, y_train_rf_probs)
knn_fpr, knn_tpr, knn_thresholds = roc_curve(y_train, y_train_knn_probs)
# Calculate the AUC score for the ROC curve
rf_auc = roc_auc_score(y_train, y_train_rf_probs)
knn_auc = roc_auc_score(y_train, y_train_knn_probs)
# Plot the ROC curve
plt.plot(rf_fpr, rf_tpr, label=f"Random Forest (AUC = {rf_auc:.2f})")
plt.plot(knn_fpr, knn_tpr, label=f"KNN (AUC = {knn_auc:.2f})")
plt.plot([0, 1], [0, 1], 'k--') # Diagonal line for random classifier
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.title('ROC Curve - Train Set')
plt.legend(loc='lower right')
plt.show()
这段代码生成了 10 个特征和 1000 个样本的随机数据。然后标准化特征,将数据拆分为训练集和测试集,使用随机森林和 KNN 分类器拟合训练集,预测正类的概率,计算每个分类器的 ROC 曲线和 AUC 评分,最后绘制训练集的 ROC 曲线。
如果我们运行上面生成的代码,我们得到:
上述生成的代码产生的 ROC 曲线。图片由 Federico Trotta 提供。
所以,这里是过程:
-
使用
NumPy
创建特征和标签。 -
特征和标签数组变成了
X
和y
。 -
特征和标签数组被分割成训练集和测试集。然后,训练集已使用 KNN 和随机森林分类器进行拟合。
-
Matplotlib
库已被用于绘制 ROC 曲线,使我们能够轻松比较两个分类器的性能。
再次,我们有一个直接的工作流,使用了所有我们需要的库,打印出我们想要的结果,无需“额外操作”。
结论
在这篇文章中,我们已经看到使sklearn
成为完美 ML 库的前 5 个特点。如在示例中所见,工作流清晰直接,使我们能够在没有额外操作的情况下进行分析。
虽然我们领域中一些其他库(如PyCaret
,它毕竟是一个低代码库)正在增长,但sklearn
无疑拥有我们开发算法所需的一切,不必担心其他方面,如集成、操作等。
免费 Python 电子书:
开始学习 Python 数据科学但感到困难? 订阅我的新闻通讯,获取我的免费电子书:这将为你提供正确的学习路径,以便通过实际操作学习 Python 数据科学。
喜欢这个故事?通过我的推荐链接成为 Medium 会员,每月只需 5 美元 点击这里:我将赚取一小部分佣金,你无需支付额外费用。
[## 通过我的推荐链接加入 Medium - Federico Trotta
阅读 Federico Trotta 的每一个故事(以及 Medium 上成千上万其他作家的故事)。你的会员费将直接支持…
federicotrotta.medium.com](https://federicotrotta.medium.com/membership?source=post_page-----d69a899267c5--------------------------------)
数据科学家的工具箱:解析
如果您有合适的工具,解析复杂文档将会变得很容易
·
阅读更多 发表在向数据科学迈进 ·9 分钟读·2023 年 11 月 11 日
--
本文讨论的基于 Python 的新 rd2md 解析器和转换器的源代码。图片由作者提供。
对于许多数据科学家来说,将复杂文档转换为可用数据是一个常见问题。让我们看看一个复杂的文档,并探讨不同的转换数据的方法。
简介;
我们将在开发复杂解析器的过程中探索这些规则:
Rule 1: Be lazy; don’t do any more than is what is needed
Rule 2: Start with the easy parts of the problem.
Rule 3: Don’t be afraid to throw away code and start over!
Rule 4: Use the simplest method possible to get the job done.
问题
作为一家机器学习公司的研究主管,我经常面临各种问题,需要进行探索和解决方案设计。上周出现了一个有趣的小问题:我们需要一种方法来为我们的开源 R SDK生成 markdown 文档,以便机器学习实验记录重要细节。我们需要一个快速的解决方案,而不花费太多时间。
这个问题可能比数据科学家每天遇到的稍微复杂一些,但它将作为一个很好的示例,展示如何使用不同的解析方法。而且作为额外奖励,我们将得到一个填补特定领域的开源项目。让我们深入了解一下吧!
在听到这个问题后,我的第一个研发规则开始起作用:
规则 1:要懒惰;只做必要的事情(懒惰被拉里·沃尔认为是程序员的三大美德之一)。
所以我开始查看将 R 代码转换为 markdown 是否已经解决了。看来确实解决了!然而,在尝试了我能找到的所有可用程序(如 R 的旧版 Rd2md)之后,它们都无法工作,且 git 仓库也不再活跃。好吧,我只能靠自己了。如果我是一名更好的 R 程序员,我可能会尝试修复现有的解决方案。但我更喜欢 Python,认为它是一个很好的解析示例。所以,是的,我们将用 Python 解析 R 文档。
所以,我开始编写一些代码。这让我想起了我的下一个研发规则:
规则 2:从问题的简单部分开始。
规则 2 可能只是我满足需要一些即时反馈的方式。但它也解决了一个更重要的问题:如果你从简单的部分开始,也许困难的部分就不会那么难。它还作为一个热身,开始解决一个问题。我通常在编码解决方案时有一两个错误的开始。这导致了我的下一个规则:
规则 3:不要害怕丢弃代码并重新开始!
最后,当你走在正确的道路上时,最后一个规则是:
规则 4:使用最简单的方法完成工作。
好的,那么将 R 文档文件转换为 markdown 的最简单方法是什么?首先,什么是 R 文档文件?R 文档直接从 R 代码转换为类似于LaTeX的东西。这是一个示例(文件以 .Rd 结尾):
% Generated by roxygen2: do not edit by hand
% Please edit documentation in R/experiment.R
\name{Experiment}
\alias{Experiment}
\title{A Comet Experiment object}
\description{
A comet experiment object can be used to modify or get information about an active
experiment. All methods documented here are the different ways to interact with an
experiment. Use \code{\link[=create_experiment]{create_experiment()}} to create or \code{\link[=get_experiment]{get_experiment()}} to
retrieve a Comet experiment object.
}
目标是将 LaTeX 转换为看起来像这样的 markdown:
## Description
A comet experiment object can be used to modify or get information about an active
experiment. All methods documented here are the different ways to interact with an
experiment. Use `create_experiment()` to create or `get_experiment()` to
retrieve a Comet experiment object.
渲染效果如下:
示例的 markdown 输出。图片由作者提供。
好吧,我们从一个非常简单的开始。我们将逐行分析 Rd 文件,像这样:
doc = Documentation()
...
for line in lines:
if line.startswith("%"):
pass
elif line.startswith("\\name{"):
matches = re.search("{(.*)}", line)
groups = groups()
name = groups[0]
doc.set_name(name)
...
在这段代码中,我们查看一行是否以“%”开头,如果是,我们就跳过它(它只是 Rd 文件中的注释)。同样,如果它以“\name”开头,那么我们就设置当前文档名称。请注意,如果我们不使用“raw” Python 字符串,则需要转义反斜杠。代码re.search(“{(.*)}”, line)假设这一行会包含结束的大括号。在我们的 SDK 中所有示例中,这一假设都是成立的,所以按照规则 3,我不会让这段代码变得更复杂。
请注意,我们在处理文件中的行之前构造了一个Documentation() 实例。我们这样做是为了收集所有部分,然后在最后调用doc.generate()。我们这样做(而不是实时生成 markdown)是因为我们解析的一些项在 markdown 中的顺序可能会不同。
我们可以以完全相同的方式处理一些 R 代码:在 Rd 文件中查找模式,并立即处理它。然而,让我们来看一下下一个无法以这种方式处理的部分:
\usage{
create_experiment(
experiment_name = NULL,
project_name = NULL,
workspace_name = NULL,
api_key = NULL,
keep_active = TRUE,
log_output = TRUE,
log_error = FALSE,
log_code = TRUE,
log_system_details = TRUE,
log_git_info = FALSE
)
}
使用部分总是以\usage{ 开头,并以单个 } 结束。由于是这样,我们可以利用这些事实创建一个稍微复杂一点的解析器:
...
for line in lines:
....
elif line.startswith("\\usage{"):
usage = ""
line = fp_in.readline().rstrip()
while line != "}":
usage += line + "\n"
line = fp_in.readline().rstrip()
doc.set_usage(usage)
这将逐行读取,收集\usage{}部分中的所有文本。
当我们转到下一个最复杂的部分时,我们必须开始变得有点聪明,并且首次使用“状态”这一概念。
考虑这段 LaTeX 代码:
\item{log_error}{If \code{TRUE}, all output from 'stderr' (which includes errors,
warnings, and messages) will be redirected to the Comet servers to display as message
logs for the experiment. Note that unlike \code{auto_log_output}, if this option is on then
these messages will not be shown in the console and instead they will only be logged
to the Comet experiment. This option is set to \code{FALSE} by default because of this
behavior.}
这很棘手。顶层格式是:
\item{NAME}{DESCRIPTION}
然而,DESCRIPTION 本身可以包含大括号项。如果你有这一段代码作为字符串(即使包含换行),你可以使用 Python 的 re(正则表达式)模块,如下所示:
text = """\item{log_error}{If \code{TRUE}, all output from 'stderr' (which includes errors,
warnings, and messages) will be redirected to the Comet servers to display as message
logs for the experiment. Note that unlike \code{auto_log_output}, if this option is on then
these messages will not be shown in the console and instead they will only be logged
to the Comet experiment. This option is set to \code{FALSE} by default because of this
behavior.}"""
matches = re.search("{(.*)}{(.*)}", text, re.DOTALL)
你可以通过matches.groups() 获取 NAME 和 DESCRIPTION。正则表达式模式“{(.)}{(.)}”中的括号表示匹配两个组:第一个组位于第一个大括号集之间,第二个组位于下一个大括号集之间。这很好用,前提是text 只是那个部分。为了能够在不先拆分该部分的情况下进行解析,我们实际上必须逐个字符地解析文本。但这并不难。
这是一个小函数,它将在给定文件指针的情况下获取多个大括号部分(在现代 Python 行话中也称为“类文件”):
def get_curly_contents(number, fp):
retval = []
count = 0
current = ""
while True:
char = fp.read(1)
if char == "}":
count -= 1
if count == 0:
if current.startswith("{"):
retval.append(current[1:])
elif current.startswith("}{"):
retval.append(current[2:])
else:
raise Exception("malformed?", current)
current = ""
elif char == "{":
count += 1
if len(retval) == number:
return retval
current += char
在函数get_curly_contents()中,你需要传入大括号部分的数量和一个文件指针。因此,要从文件中获取 2 个大括号部分,你可以这样做:
fp = open(FILENAME)
name, description = get_curly_contents(2, fp)
get_curly_contents() 是这个项目中几乎最复杂的部分。它有三个状态变量:retval、count 和 current。retval 是已解析部分的列表。count 是当前大括号项的深度。current 是当前正在处理的内容。这个函数实际上在一些地方非常有用,正如我们将看到的那样。
最后,还有一个更复杂的层面。问题区域是 R 类定义中的Method子章节。这是一个简化的示例:
\if{html}{\out{<hr>}}
\if{html}{\out{<a id="method-Experiment-new"></a>}}
\if{latex}{\out{\hypertarget{method-Experiment-new}{}}}
\subsection{Method \code{new()}}{
Do not call this function directly. Use \code{create_experiment()} or \code{get_experiment()} instead.
\subsection{Usage}{
\if{html}{\out{<div class="r">}}\preformatted{Experiment$new(
experiment_key,
project_name = NULL
)}\if{html}{\out{</div>}}
}
\subsection{Arguments}{
\if{html}{\out{<div class="arguments">}}
\describe{
\item{\code{experiment_key}}{The key of the \code{Experiment}.}
\item{\code{project_name}}{The project name (can also be specified using the \code{COMET_PROJECT_NAME}
parameter as an environment variable or in a comet config file).}
}
\if{html}{\out{</div>}}
}
}
这很复杂,因为我们有嵌套的部分:Usage 和 Arguments 在 Method 里面。我们将为这次解析拿出全部的解析工具。
为了简化这个过程,我们首先要做的是“分词”Method子章节。这是一个将文本拆分成相关字符串的 fancy 词汇。例如,考虑这个 LaTeX 文本:
\subsection{Usage}{
\if{html}{\out{<div class="r">}}\preformatted{Experiment$new(
experiment_key,
project_name = NULL
)}\if{html}{\out{</div>}}
}
它可以被分解成一个字符串列表,如下:
[
"\\", "subsection", "{", "Usage", "}", "\\", "if",
"{", "html", "}", "{", "\\", "out", "{", "<", "div",
" ", "class", "=", "\"r\"", ">", "}", "}", "\\",
"preformatted", "{", "Experiment$new", "(", "experiment_key",
"project_name", "=", "NULL", ")", "}", "\\", "if",
"{", "html", "}", "{", "\\", "out", "{", "<", "/", "div",
">", "}", "}", "}"
]
一组分词字符串使你能够轻松地将其处理成子部分。此外,你还可以轻松地“前瞻”一个或多个标记,以查看接下来会发生什么。这对于正则表达式或处理单个字符而非标记时可能很困难。以下是解析分词部分的一个示例:
doc = Documentation()
...
method = Method()
position = 0
preamble = ""
tokens = tokenize(text)
while position < len(tokens):
token = tokens[position]
if token == "\\":
if tokens[position + 1] == "subsection":
in_preamble = False
if tokens[position + 3] == "Usage":
position, usage = get_tokenized_section(
position + 5, tokens
)
method.set_usage(usage)
elif tokens[position + 3] == "Arguments":
# skip this, we'll get with describe
position += 5
elif tokens[position + 3] == "Examples":
position, examples = get_tokenized_section(
position + 5, tokens
)
method.set_examples(examples)
elif tokens[position + 3] == "Returns":
position, returns = get_tokenized_section(
position + 5, tokens
)
method.set_returns(returns)
else:
raise Exception("unkown subsection:", tokens[position + 3])
elif tokens[position + 1] == "describe":
position, describe = get_tokenized_section(position + 2, tokens) # noqa
method.set_describe(describe)
else:
# \html
position += 1
else:
if in_preamble:
preamble += token
position += 1
method.set_preamble(preamble)
doc.add_method(method)
就是这样!要查看完成的项目,请查看新的基于 Python 的 rd2md。它是一个 pip-installable,开源的 Python 库,用于从 R 的 Rd 文件生成 markdown。我们在这里使用了它的 R 文档:
www.comet.com/docs/v2/api-and-sdk/r-sdk/overview/
这是一个下午的临时小项目吗?是的。它由不低于 4 种不同的解析方法组成。但它能完成任务,而且据我所知,它是唯一有效的 Rd 到 markdown 转换器。如果我要重构它,我可能会先分词整个文件,然后使用上面展示的最后一种方法进行处理。记住规则 3:不要害怕丢弃代码并重新开始!
如果你想为 GitHub 仓库做贡献,请随意。如果你有问题,请在 Issues 中告诉我们。
对人工智能、机器学习和数据科学感兴趣?考虑点赞和关注一下吧。Doug 是 comet.com的研究主管,该公司专注于机器学习实验跟踪和模型监控。
GPT-4 的去污染评估
GPT-4 不会很快成为你的律师
·
关注 发表在 数据科学前沿 ·7 分钟阅读·2023 年 3 月 27 日
--
图片来源:Pixabay
GPT-4 在三月由 OpenAI 宣布,伴随令人印象深刻的展示和突出的声明。
这些声明大多数来自他们对 GPT-4 的自我评估。
OpenAI 使用了许多现有的专业和学术考试进行评估。
但在公共基准测试中评估大型语言模型是极具挑战性的。
像 GPT-4 这样的模型可能会受到“数据污染”,即它们可能在评估数据上进行了训练。
为什么这是个问题?
让我们举个例子。
GPT-4 在 LSAT 考试中进行了评估。为了进行科学可信的评估,OpenAI 必须检查用于评估的 LSAT 问题是否不在 GPT-4 的训练数据中。如果在其中,GPT-4 可能已经记住了这些问题,那么在评估时显然会在这些特定问题上表现更好。
就像一个在考试前已经知道考试题目的人。
你可以说这就像是作弊。
在GPT-4 技术报告中,OpenAI 透露了关于 GPT-4 的少数几件事,其中之一是他们评估中的数据污染。他们公开了量化和评估这种污染的策略,并从观察中得出了几个结论。
在这篇文章中,我回顾和讨论了 OpenAI 如何处理 GPT-4 的数据污染。我揭示了他们方法中的几个陷阱。
我无法同意他们的几个结论。
评估数据的去污染
为了检查训练数据和评估数据之间是否存在交集,OpenAI 使用了一种依赖于子字符串匹配算法的非常简单的技术(在技术报告第 28 页描述)。
首先,他们删除了训练数据和评估数据(考试)中的所有空格和符号。他们保留了数字。
然后,他们随机挑选了考试中每个问题的 3 个 50 字符的子字符串(或等效的)。如果这些子字符串中的任何一个恰好出现在 GPT-4 的训练数据中,则该问题会从评估数据中删除。
使用这种方法,他们做出了两个关键选择。
首先,这种方法是随机的。
对于问题非常长的考试来说,选择 3 个随机子字符串尤其具有问题。
例如,统一律师资格考试中的一个问题可能包含 1,500 个 50 字符的序列。注意:它们是非常长的问题, 查看一些示例。
在 1,500 个子字符串中随机选择 3 个,这意味着该去污染策略完全忽略了每个问题的大部分内容。
这种策略无法可靠地检测出问题的大部分是否存在于训练数据中。
我们可以想象这些考试问题中的一些可能已经在 GPT-4 训练数据中被研究或讨论过,但只是部分而非全部,因为它们是非常长的问题。因此,在这种情况下,部分但重要的匹配将不会被检测到。
统一律师资格考试有 400 道题目。但是通过随机检查每个问题的 3 个子字符串,OpenAI 并未发现这些问题出现在训练数据中。
第二个关键选择是他们去污染了评估数据,而不是训练数据。
从训练数据中删除问题,重新训练 GPT-4,然后再在考试中评估它显然会花费过高。
然而,如果他们在开发过程中早些时候评估了这种污染,即在训练之前,他们本可以从训练数据中移除所有的考试示例。
还要注意,他们在去污染过程中没有包括 RLHF 的数据。如果一个考试的问题在 RLHF 中,它将保留在评估数据中。
定义
RLHF 代表来自人类反馈的强化学习。GPT-4 在预训练后,通过在人工反馈上进行强化学习进一步微调,以提高其性能。这个“反馈”数据集没有被检查以进行去污染。
不包括 RLHF 训练数据的主要原因是利用 RLHF 进行微调并未显著提高 GPT-4 的性能。它们仅观察到在 RLHF 后训练的平均得分提高了+0.3%。
它是被污染的
图片来自 Pixabay
每个考试的污染细节在报告的第 30 页中给出。
在用于评估的 49 个考试中,发现有 12 个完全没有出现在训练数据中。它们是:所有 Leetcode 数据集、Uniform Bar Exam、SAT EBRW 考试和一些 AP 考试。
总体而言,用于评估的考试包含 4,123 个问题。其中 545.5个问题已在训练数据中找到。注意:为什么会有“.5”?据我了解,如果有匹配,OpenAI 会完全移除该问题。但对于考试“USA Biolympiad Semifinal Exam 2020”,包含 150 个问题,他们指出他们移除了 3.00%的问题(见论文的表 10)。150 的 3%是 4.5。这些数字中的一个可能是错误的。
这是 13.2%的评估数据被污染。
有趣的是,对于几个考试,去污染似乎改善了 GPT-4 的结果。
这与直觉相反。
我们可能认为,如果被移除的问题在训练数据中,GPT-4 应该擅长回答它们,因为它有机会记住它们。
但我们对这些被排除的问题一无所知。
对于一些考试来说,它们可能是最困难的,因此在从评估中排除这些问题后正确答案的百分比更高。
OpenAI 声称污染没有显著影响。他们指出:
总体来看,大多数考试中,污染和视力的影响相对较小。 (表 9 的标题)
退化通常很小,正面和负面效果一样多 […] (表 10 的标题)
这是“总体”结论。如果我们更仔细地查看结果,这并不那么明显。让我们看看一些细节。
在技术报告的表 10 中,OpenAI 还在每个考试中对两个独立的问题集合评估了 GPT-4:
-
“contaminated”:这个集合仅包含在训练数据中找到的问题。
-
“non-contaminated”:这个集合包含了所有剩余的问题。
这是一个有趣的实验。GPT-4 在这两种数据集(第 5 和第 6 列)上的表现对某些考试变化极大,例如 AMC 12 的表现从 41.67%到 0%。
对于其他一些考试,GPT-4 在没有使用的评估数据(未受污染)上的表现更好。
这是否意味着 GPT-4 在训练期间未见过的问题上表现更好?
不,“受污染”和“未受污染”只是两种不同的评估数据。
GPT-4 可能因多种原因在两个数据集中的表现不同,例如问题的主题、长度、难度等。
GPT-4 对这些考试表现如何?
让我们具体看看 LSAT 考试。假设 160 以上的分数在此考试中是一个好分数。
GPT-4 获得了 163 的分数。在去污染后,移除 39%的问题,GPT-4 获得了更高的 167 分。
我们可以得出结论,GPT-4 能在 LSAT 考试中取得好分数吗?
是的,我们可以。但前提是允许作弊。
一方面,我们有完整的考试,GPT-4 的分数为 163。这是一个好分数,但 GPT-4 在考试前见过一些问题。
另一方面,如果我们移除 39%的问题进行去污染,这就不再是 LSAT 考试了。没有人能通过 61%的 LSAT。这种考试并不存在。
此外,移除的 39% 问题可能包含最难的问题。我们不知道在这 61%的 LSAT 中,167 的分数好坏如何。
对于所有其他用于评估的“受污染”考试,我们可以类似地推理。
一些考试没有“受污染”,例如统一律师资格考试和 Leet 代码问题,但还有其他问题。
我不会在这里讨论这些问题。Arvind Narayanan 和 Sayash Kapoor 已经在他们的权威文章中讨论了这些问题的结果,你可以在这里阅读:
我们不知道答案,但我们希望能将一些现实注入对话中。OpenAI 可能违反了…
aisnakeoil.substack.com](https://aisnakeoil.substack.com/p/gpt-4-and-professional-benchmarks?source=post_page-----38a27fc45c30--------------------------------)
结论
正如我在介绍中写的,评估大型语言模型的数据污染是一个极其困难的任务。
在收集和预处理训练数据时,理想情况下我们应该已经确定了一份需要从训练数据中排除的公共相关考试和基准的清单。
尽管如此,我的观点是,OpenAI 训练 GPT-4 时包含所有这些考试实际上是非常有意义的。
目标也是让 GPT-4 尽可能地适应这些考试提出的问题。我可以看到 GPT-4 在这个领域的许多潜在应用,比如帮助学生和老师准备考试。
然而,这个选择是有代价的:我们不能用这些考试来以科学的可信度评估 GPT-4。
如果你喜欢这篇文章并且有兴趣阅读接下来的文章,支持我的工作的最佳方式是通过这个链接成为 Medium 会员:
[## 通过我的推荐链接加入 Medium - Benjamin Marie
阅读 Benjamin Marie 的每一个故事(以及 Medium 上的其他成千上万的作家)。你的会员费直接支持…
medium.com](https://medium.com/@bnjmn_marie/membership?source=post_page-----38a27fc45c30--------------------------------)
如果你已经是会员并且想要支持这项工作, 只需在 Medium 上关注我。
从基础逻辑门到深度神经网络:权威感知机教程
原文:
towardsdatascience.com/the-definitive-perceptron-guide-fd384eb93382
朝着掌握 AI 的方向前进
数学、二分类、逻辑门等
·发布于 Towards Data Science ·阅读时间 21 分钟·2023 年 4 月 28 日
--
TL;DR
感知机的世界令人着迷,因为这些模型是现代人工智能的基础。在这篇博客文章中,我们将简明扼要地讲述感知机的故事,从它的神经网络起源到其演变为多层感知机及更高级的模型。我们将探讨驱动这个模型的基本数学,使其能够作为二分类器、模拟计算机晶体管、乘法器和逻辑门。此外,我们还将考察感知机模型如何为更高级的分类器奠定基础,包括逻辑回归、支持向量机和深度学习。我们将提供示例代码片段和插图以增强理解。此外,我们还将使用实际案例来了解何时以及如何使用感知机模型。
本指南是任何对数据科学感兴趣的人的宝贵资源,无论其专业水平如何。我们将探讨感知机模型,这一模型自人工智能早期便存在,并且至今依然相关。我们将深入了解其历史、工作原理及与其他模型的比较。此外,我们还将构建模型和逻辑门,并提供对未来发展的见解。无论你是自学的数据科学家、AI 从业者还是有经验的机器学习专业人士,你都会在这本全面的指南中找到有价值的内容。
目录
1. 介绍
1.1 感知机模型的简史
1.2. 感知机模型在机器学习中的重要性
2. 感知机模型背后的数学
2.1. 线性可分性
2.2. 感知机学习算法
2.3. 感知机收敛定理
3. 感知机模型作为二分类器
3.1. 线性分类
3.2. 感知器模型的局限性
3.3. 感知器模型的多类分类
4. 逻辑门与感知器模型
4.1. 感知器如何用于生成逻辑门
4.2. 示例:使用感知器实现 NAND 门
4.3. 扩展到其他逻辑门:AND、OR、XOR
5. 感知器用于乘法和晶体管类似功能
5.1. 感知器与晶体管的类比
5.2. 使用感知器进行乘法
5.3. 感知器的未来与硬件实现
6. 比较感知器模型与逻辑回归
6.1. 感知器与逻辑回归的相似性
6.2. 感知器与逻辑回归的差异
6.3. 在感知器与逻辑回归之间的选择
7. 感知器模型的创意与独特应用
7.1. 光学字符识别(OCR)
7.2. 音乐类型分类
7.3. 入侵检测系统
7.4. 情感分析
8. 感知器模型的演变及其在深度学习中的遗产
8.1. 感知器到多层感知器(MLPs)的演变
8.2. 深度学习与感知器的遗产
8.3. 感知器与深度学习的未来
9. 结论
· 参考文献
· 联系方式
1. 介绍
1.1 感知器模型的简史
Warren McCulloch 和 Walter Pitts 在 1943 年的人工神经元研究[1]启发了一位名叫 Frank Rosenblatt 的心理学家在 1957 年制造了感知器模型[2]。Rosenblatt 的感知器是第一个用算法描述的神经网络(NN),为现代机器学习(ML)技术铺平了道路。发现后,感知器受到了科学家和公众的广泛关注。有些人认为这一新技术对智能机器至关重要——一个学习和改变的模型[3]。
然而,感知器的受欢迎程度并没有持续。然后,在 1969 年,Marvin Minsky 和 Seymour Papert 出版了他们的书《感知器》,书中强调了感知器模型的局限性,同时揭示了它无法解决像 XOR 分类这样的难题[4](第三部分)。这项工作引发了对神经网络的重大兴趣丧失,转而关注其他方法。感知器的早期历程列于图 1。
图 1. 感知器历史上的重要里程碑(1943–1982)。图由作者创作。
虽然花费了十多年时间,但 1980 年代对神经网络的兴趣得以重新点燃。部分感谢于 Rumelhart、Hinton 和 Williams 通过反向传播算法引入的多层神经网络训练[5](第五部分)。
2012 年,计算能力、大数据、RELU 等非线性激活以及 dropout 技术的重大进展促成了最全面的卷积神经网络的诞生。ImageNet 提供的大型标注数据集在填充其容量方面发挥了重要作用。
图 2. 感知机历史上的重要里程碑(1985–1997)。图由作者绘制。
今天对深度学习的狂热由此兴起。因此,感知机模型在其基础中扮演了关键角色—图 2和图 3列出了剩余的里程碑(图 1的延续)。
图 3. 感知机历史上的重要里程碑(2006–2018)。图由作者绘制。
1.2. 感知机模型在机器学习中的重要性
尽管有其局限性,感知机模型仍然是机器学习中的一个重要构建块。它是人工神经网络的基础组成部分,现在这些网络被广泛用于各种应用,从图像识别到理解人类语言。
感知机模型的简洁性使其成为新手入门机器学习的绝佳起点。它使线性分类和从数据中学习变得易于理解。此外,感知机算法可以很容易地修改以创建更复杂的模型,例如多层感知机(MLP)和支持向量机(SVM),这些模型可以在更多情况下使用,并解决许多原始感知机模型无法解决的问题。
在接下来的部分,我们将介绍感知机模型背后的数学,如何将其用作二分类器和构建逻辑门,以及如何用于执行类似计算机晶体管的乘法任务。我们还将讨论感知机模型与逻辑回归之间的区别,并展示感知机模型如何以新颖和令人兴奋的方式使用。
2. 感知机模型背后的数学
2.1. 线性可分性
从本质上讲,感知机模型是一个线性分类器。它旨在找到一个“超平面”(二维空间中的一条线、三维空间中的一个平面,或更高维度的类似物)来分隔两个数据类别。为了使数据集具有线性可分性,超平面必须正确地分类所有数据点[6]。
从数学上讲,感知机模型可以表示如下:
y = f(w * x + b)
。
x
是输入向量;w
是权重向量;b
是偏置项;f
是激活函数。在感知机的情况下,激活函数是一个阶跃函数,将输出映射为 1 或 0,表示两个类别(图 4)。
图 4. 单位阶跃函数的描述,包含将输出映射为 0 或 1 的分段条件。图由作者绘制。
感知机模型可以扩展到具有多个输入特征x
,定义如下:
y = f(w_1 * x_1 + w_1 * x_1 ... w_n * x_n + b)
。
上述方程及其输出的阶跃函数被激活(即,通过 0 关闭或通过 1 打开),如下图所示,图 5。
图 5. 多变量线性分类。注意加权和通过激活函数,上述阶跃函数——来源 link。
2.2. 感知器学习算法
感知器学习算法是一种保持权重和偏置最新以减少分类错误的方法[2]。该算法可以总结如下:
-
将权重和偏置初始化为小的随机值。
-
对于每对输入输出
(x, d)
,计算预测输出y = f(w * x + b)
。 -
根据误差
e = d - y
更新权重和偏置:
w = w + η * e * x
b = b + η * e
,
其中η
是学习率,一个小的正数,控制更新的步长。
- 对固定次数的迭代或直到误差收敛,重复步骤 2 和 3。
我们可以使用 Python 和 Sklearn 快速实现上述步骤:
import numpy as np
from sklearn.linear_model import Perceptron
X = np.array([2, 3], [1, 4], [4, 1], [3, 2])
y = np.array([1, 1, 0, 0])
perceptron = Perceptron()
perceptron.fit(X, y)
然后,使用拟合的模型,我们可以进行如下预测:
new_data_point = np.array([[1, 2]])
prediction = perceptron.predict(new_data_point)
print(prediction)
如果数据是线性可分的,[7]中的感知器学习算法保证收敛。
图 6. 布尔分类,其中类别是线性可分的。图像由作者创建。
2.3. 感知器收敛定理
1960 年,Rosenblatt 证明了感知器收敛定理。该定理指出,如果数据集可以线性分隔,感知器学习算法将在有限的步骤内找到解决方案[8]。该定理表明,只要时间足够,感知器模型将找到最佳的权重和偏置,以对所有数据点进行线性分隔的分类。
但如果数据集不是线性可分的,感知器学习算法可能找不到合适的解决方案或收敛。因此,研究人员开发了更复杂的算法,如多层感知器和支持向量机,这些算法可以处理不能直线分隔的数据[9]。
3. 感知器模型作为二分类器
3.1. 线性分类
如前所述,感知器模型是一种线性分类器。它创建了一个决策边界,这是一个特征空间中的直线,用于分隔两个类别[6]。当添加新数据点时,感知器模型根据其在决策边界上的位置对其进行排序。感知器运行快速且易于使用,因为它简单,但只能解决数据可以线性分隔的问题。
3.2. 感知器模型的局限性
感知器模型的一个大问题是它不能处理无法用直线分隔的数据。异或问题是一些数据集无法通过单个超平面分隔的例子,这使得感知器无法找到解决方案[4]。研究人员开发了更高级的方法来绕过这个问题,例如多层感知器,它们有多个神经网络层,能够学习进行不沿直线的决策[5]。
感知器模型对学习率和初始权重的设置也很敏感。例如,如果学习率过低,收敛可能会很慢,而较大的学习率可能会导致振荡或发散。同样,初始权重的选择会影响解决方案的收敛速度以及最终效果[10]。
3.3. 感知器模型的多类分类
尽管基本的感知器模型是为两类问题设计的,但通过训练多个感知器分类器(每个类别一个),它可以解决多于两类的问题[11]。最常见的方法是“一对多(OvA)”,其中训练一个单独的感知器来区分各类。然后,在分类新数据点时,选择输出值最高的感知器作为预测类别。
另一种方法是“一对一(OvO)”方法,其中对每对类别训练一个感知器。最终的分类决策是通过投票机制做出的,每个感知器对其预测的类别进行投票,票数最多的类别被选择。虽然 OvO 需要训练比 OvA 更多的分类器,但每个感知器只需要处理数据的一个较小子集,这对大型数据集或高计算复杂度的问题可能更有利。
4. 逻辑门与感知器模型
4.1. 感知器如何用于生成逻辑门
感知器模型可以用来表示逻辑门,这些逻辑门是数字电路的最基本组成部分。通过适当调整感知器的权重和偏置,它可以被训练执行逻辑操作,如与(AND)、或(OR)和非(NOT)[12]。感知器与逻辑门之间的联系表明,神经网络不仅可以进行计算,还具有模拟复杂系统的潜力。
图 6. 线性可分逻辑门:与(AND) 和 或(OR)(分别为左侧和中间)。另一方面,异或(XOR) 不能通过单一线性分类器(右侧)进行分离,但可以通过两层网络进行分离(稍后会详细介绍)——该图由作者创建。
4.2. 示例:使用感知器实现 NAND 门
NAND 门是一个基本的逻辑门,只有当两个输入都为 1 时,输出才为 0,在其他情况下输出为 1。NAND 门的真值表如下:
NAND 门真值表。表格由作者创建。
要使用感知器实现 NAND 门,我们可以手动设置权重和偏置,或使用感知器学习算法来训练感知器。以下是可能的权重和偏置配置:
w1 = -1
;
w2 = -1
;
b = 1.5
。
使用这些参数,感知器可以表示为:
y = f((-1 * A) + (-1 * B) + 1.5)
。
图 6. 训练数据、图形表示以及 AND 门的线性函数。图由作者创建。
在这里,f
是步进函数,A
和 B
是输入。如果使用真值表中的值测试此设置,你将从 NAND 门获得正确的结果:
图 7. 逻辑 NAND 的真值表,以及上述训练的感知器的输出和作者创建的结果。
在 Python 中,NAND 可以如下实现:
def nand_gate(x1, x2):
w1, w2, b = -1, -1, 1.5
return int(w1 * x1 + w2 * x2 + b > 0)
binary_inputs = [(0,0), (0,1), (1,0), (1,1)]
for A and B in binary_inputs:
print(f"(A, B) --> nand_gate(A, B)")
正如预期的那样,重现上述总结 NAND 门的表格
(0, 0) --> 1
(0, 1) --> 1
(1, 0) --> 1
(1, 1) --> 0
NAND 门可以用于构建所有其他门,因为它在功能上是完整的,这意味着任何其他逻辑函数都可以仅使用 NAND 门来推导。以下是如何使用 NAND 门创建一些基本门的简要说明:
-
NOT 门:将 NAND 门的两个输入连接到输入值。
-
AND 门:首先创建一个 NAND 门,然后将输出通过一个 NOT 门。
-
OR 门:在将每个输入馈送到 NAND 门之前,对每个输入应用 NOT 门。
要创建一个接受任意数量输入的 NAND 门,可以使用 Python 定义一个函数,该函数接受一个输入列表并返回 NAND 输出。以下是演示此操作的代码片段:
def nand_gate(inputs):
assert len(inputs) > 1, "At least two inputs are required."
# Helper function to create a 2-input AND gate
def and_gate (x1, x2):
w1, w2, b = 1, 1, -1.5
return int(w1 * x1 + w2 * x2 + b > 0)
Reduce the inputs to a single NAND output using the helper function.
result = and_gate(inputs[0], inputs[1])
for i in range (2, len (inputs)):
result = and_gate(result, inputs[i])
return 0 if result > 0 else 1
# Example usage
inputs = [(0, 0, 0, 0),
(0, 0, 0, 1),
(0, 0, 1, 0),
(0, 0, 1, 1),
(0, 1, 0, 0),
(0, 1, 0, 1),
(0, 1, 1, 1),
(1, 0, 0, 0),
(1, 0, 0, 1),
(1, 0, 1, 0),
(1, 0, 1, 1),
(1, 1, 0, 0),
(1, 1, 0, 1),
(1, 1, 1, 0),
(1, 1, 1, 1)]
for A0, A1, A2, and A3 inputs:
output = nand_gate((A0, A1, A2, A3))
print(f"({A0}, {A1}, {A2}, {A3}) --> {output}")
该函数使用一个辅助函数(即and_gate
)来创建一个具有两个或更多输入的 NAND 门。然后在给定的输入上重复 AND 操作。最终结果是 NAND 门的输出,具有任意数量的输入位,即 AND 门的取反值。
4.3. 扩展到其他逻辑门:AND、OR、XOR
类似地,感知器可以建模其他逻辑门,如 AND、OR 和 NOT。例如,具有权重 w1 = 1
、w2 = 1
和 b = -1.5
的感知器可以表示一个 AND 门。
def and_gate(x1, x2):
w1, w2, b = 1, 1, -1.5
return int(w1 * x1 + w2 * x2 + b > 0)
binary_inputs = [(0,0), (0,1), (1,0), (1,1)]
for A, B in binary_inputs:
print(f"({A}, {B}) --> {and_gate(A, B)}")
再次,输出模仿预期的 AND 门。
(0, 0) --> 0
(0, 1) --> 0
(1, 0) --> 0
(1, 1) --> 1
然而,单个感知器无法建模 XOR 门,因为 XOR 门不是线性可分的。相反,必须使用多层感知器或感知器组合来解决 XOR 问题[5]。
5. 用于乘法和类似晶体管功能的感知器
5.1. 感知器与晶体管之间的类比
晶体管是电子设备的基本构建块。它们负责执行像加法和乘法这样的简单任务。有趣的是,感知器也可以被视为展示类似功能的计算单元。例如,感知器在机器学习和人工神经元中被使用。相比之下,晶体管是物理部件,改变电信号的流动 [13]。尽管如此,正如上一节所示,这两种系统都可以建模和执行逻辑运算。
5.2. 使用感知器进行乘法运算
我们可以利用感知器的二进制运算能力来执行乘法。例如,考虑两个二进制位(即A
和B
)的展开,可以将其表示为一个简单的 AND 门。正如第四部分所示,AND 门可以用感知器建模。
但是,对于涉及多个二进制位的更复杂的乘法任务,我们需要添加更多的部件,如半加器和全加器,这些部件需要逻辑门的组合 [14]。使用感知器来构建这些部件使得构建可以执行二进制乘法的人工神经网络成为可能。
例如,假设我们要乘以两个 2 位的二进制数,A1A0
和 B1B0
。那么,我们可以将乘法分解为一系列的 AND 运算和加法:
-
计算部分乘积:
P00 = A0 * B0
、P01 = A0 * B1
、P10 = A1 * B0
和P11 = A1 * B1
。 -
使用半加器和全加器将部分乘积相加,得到一个 4 位的二进制乘积。
每个 AND 运算和加法都可以通过感知器或表示所需逻辑门的感知器组来完成。
使用上一节中设置的 AND 门函数,我们可以在 Python 中实现基于感知器的乘法:
A1A0 = [1, 0]
B1B0 = [1, 1]
P00 = and_gate(A1A0[1], B1B0[1])
P01 = and_gate(A1A0[1], B1B0[0])
P10 = and_gate(A1A0[0], B1B0[1])
P11 = and_gate(A1A0[0], B1B0[0])
# Implement a simple adder using perceptron-based logic gates
result = [P00, P01 ^ P10, (P01 & P10) ^ P11, P11]
print(result)
5.3. 感知器及其硬件实现的未来
尽管感知器可以像晶体管一样执行基本的数学运算,但其硬件实现的效率不如传统晶体管。然而,近期在神经形态计算方面的改进表明,可能有办法制造类似于神经网络的硬件,如感知器 [15]。这些神经形态芯片可能有助于机器学习任务减少能源消耗,并开启对计算机新思维方式的探索。
6. 感知器模型与逻辑回归的比较
6.1. 感知器与逻辑回归的相似性
感知器模型和逻辑回归都是线性分类器,可以用来解决二分类问题。它们都依赖于找到一个决策边界(一个超平面),以在特征空间中分隔不同的类别 [6]。此外,它们还可以通过一对多和一对一等技术扩展以处理多分类问题 [11]。
让我们来看一下 Python 实现的区别:
from sklearn.linear_model import LogisticRegression
log_reg = LogisticRegression()
log_reg.fit(X, y)
new_data_point = np.array([[1, 2]])
prob_prediction = log_reg.predict_proba(new_data_point)
print(prob_prediction)
import numpy as np
from sklearn.linear_model import Perceptron, LogisticRegression
# Dataset
X = np.array([[2, 3], [1, 4], [4, 1], [3, 2]])
y = np.array([1, 1, 0, 0])
# Train Perceptron
perceptron = Perceptron()
perceptron.fit(X, y)
# Train Logistic Regression
log_reg = LogisticRegression()
log_reg.fit(X, y)
# New data point
new_data_point = np.array([[1, 2]])
# Perceptron prediction
perc_prediction = perceptron.predict(new_data_point)
print("Perceptron prediction:", perc_prediction)
# Logistic Regression prediction
log_reg_prediction = log_reg.predict(new_data_point)
print("Logistic Regression prediction:", log_reg_prediction)
# Logistic Regression probability prediction
prob_prediction = log_reg.predict_proba(new_data_point)
print("Logistic Regression probability prediction:", prob_prediction)
这将输出:
Perceptron prediction: [1]
Logistic Regression prediction: [1]
Logistic Regression probability prediction: [[0.33610873 0.66389127]]
6.2. 感知器与逻辑回归的区别
尽管感知器模型和逻辑回归有一些相似之处,但两者之间存在一些本质区别:
-
激活函数:感知器模型使用阶跃函数作为其激活函数,而逻辑回归使用逻辑(Sigmoid)函数[10]。这种差异导致感知器具有二元输出(
0
或1
)。与此同时,逻辑回归生成一个概率值(介于 0 和 1 之间),表示实例属于特定类别的可能性。 -
损失函数:感知器学习算法最小化误分类错误,而逻辑回归最小化对数似然或交叉熵损失[16]。这种区别使逻辑回归对数据集中的噪声和异常值更具鲁棒性,因为它考虑了错误的幅度,而不仅仅是误分类实例的数量。
-
收敛性:感知器学习算法在数据线性可分时可以收敛,但在其他情况下可能无法收敛[7]。另一方面,逻辑回归使用基于梯度的优化技术,如梯度下降或牛顿-拉夫森方法,这些方法能够保证在对数似然等凸损失函数中找到全局最优解[17]。
-
非线性可分数据:尽管感知器模型在处理非线性可分数据时会遇到困难,但逻辑回归可以通过引入高阶多项式特征或使用核方法来扩展处理非线性决策边界[18]。
6.3. 选择感知器与逻辑回归
感知器模型和逻辑回归的选择取决于问题和数据集。逻辑回归更可靠,可以处理更广泛的问题,因为它基于概率并且可以建模非线性决策边界。然而,在某些情况下,特别是处理可以线性分离的数据时,感知器模型可能更易于使用且计算资源消耗更少。
7. 感知器模型的创造性和独特应用
7.1. 光学字符识别(OCR)
感知器模型已被应用于光学字符识别(OCR)任务,其目标是识别并将打印或手写文本转换为机器编码文本[19]。感知器或其他机器学习算法通常用于 OCR 任务,以预处理将被读取的图像,从中提取特征并进行分类。感知器模型对于字符可以通过直线分离的 OCR 任务是一个不错的选择,因为它易于使用且与计算机配合良好。
7.2. 音乐类型分类
感知机也可以用于音乐流派分类,这涉及识别给定音频轨迹的流派。可以训练感知机模型将音频分类为已设置的流派 [20]。这通过提取音频信号的相关部分,如频谱特征或时间特征,然后将其组合起来来完成。尽管深度学习和卷积神经网络等更先进的方法通常能提供更好的结果,但感知机模型仍能很好地工作,特别是在只有少数几个流派或特征可以线性分离的情况下。
7.3. 入侵检测系统
入侵检测系统,或称为 IDS,广泛用于网络安全中,以查找恶意行为或未经授权的访问计算机网络。IDS 可以使用感知机作为分类器,通过查看数据包大小、协议类型和网络流量连接长度来确定活动是常规的还是恶意的 [21]。支持向量机和深度学习可能更擅长检测,但感知机模型可以用于简单的 IDS 任务或作为比较点。
7.4. 情感分析
感知机可以应用于情感分析,这是一个自然语言处理任务,旨在确定文本中表达的情感(例如,正面、负面或中性)。通过将文本转换为数值特征向量,如词频-逆文档频率(TF-IDF)表示 [22],可以训练感知机模型根据其语气分类文本。尽管更先进的技术如递归神经网络或变换器在情感分析性能上已经超越了感知机,但感知机仍然可以作为文本分类的入门方法或特定用例的简单替代方案。
8. 感知机模型的演变及其在深度学习中的遗产
8.1. 感知机到多层感知机(MLPs)的演变
感知机模型已经能够解决具有明确决策边界的问题,但在需要明确决策边界的任务中仍存在困难。多层感知机(MLPs)的引入,包含多个感知机样单位的层,标志着人工神经网络的显著进步 [5]。MLPs 可以逼近任何连续函数,只要具有足够数量的隐藏层和神经元 [23]。通过采用反向传播算法,MLPs 可以训练以解决更复杂的任务,例如 XOR 问题,这是单个感知机无法解决的。
8.2. 深度学习与感知机的遗产
感知机模型奠定了深度学习的基础,深度学习是机器学习的一个子领域,专注于具有多层(深度神经网络)的神经网络。感知机模型是卷积神经网络(CNNs)和递归神经网络(RNNs)等深度学习技术的基础,这些技术在图像分类、自然语言处理和语音识别等任务中达到了最先进的性能 [24]。
在 CNNs 中,来自感知器的加权输入信号和激活函数的思想被传递到卷积层。这些层通过对输入区域应用滤波器来学习数据中的空间层次结构。同样,RNNs 通过添加递归连接来建立在感知器模型的基础上。这使得网络能够学习序列数据中的时间依赖关系 [25]。
深度学习与其他模型的对比:Google 趋势随时间变化。图片由作者根据 Carrie Fowle’s TDS Medium 博客 (link) 制作。
8.3. 感知器与深度学习的未来
尽管基础,更多复杂的深度学习技术已经主要取代了感知器模型。然而,它仍然对机器学习有价值,因为它是一种简单而有效的方式来教授神经网络的基础知识,并为构建更复杂的模型提供了思路。随着深度学习的不断进步,感知器模型的核心思想和原则可能仍会保持不变,并影响新架构和算法的设计。
9. 结论
本博客全面探讨了感知器模型及其数学、二元分类和逻辑门生成应用。通过理解这些基础知识,我们已经解锁了在各种实际应用中利用感知器的潜力,甚至可以构建更高级的模型,如多层感知器(MLPs)和卷积神经网络(CNNs)。
我们还比较了感知器和逻辑回归,通过考察感知器作为机器学习中更高级技术的基础,突出了它们的异同。我们进一步探讨了感知器在人工智能中的作用、历史意义和持续影响。
请记住,感知器只是其中的一部分。还有无数其他模型和技术,无论是已被发现的还是待发现的,每个都有独特的优点和应用。尽管如此,借助本教程提供的坚实基础,你已准备好迎接人工智能领域中的挑战和机遇。
我希望本博客既引人入胜又富有启发性,鼓励你继续学习和尝试感知器模型及其他相关内容。拥抱你获得的新知识,让你的创造力和好奇心引导你探索人工智能和机器学习的精彩世界。请在下方分享你的想法和评论!
参考文献
[1] McCulloch, W.S., & Pitts, W. (1943). 神经活动中固有思想的逻辑演算。数学生物物理学公报,5,115–133。
[2] Rosenblatt, F. (1958). 感知器是脑中信息存储和组织的概率模型。心理学评论,65(6),386–408。
[3] 纽约时报 (1958 年 7 月 8 日). 一种新型海军设备通过实践学习. 纽约时报.
[4] Minsky, M. 和 Papert, S. (1969). 感知器:计算几何入门, MIT Press.
[5] Rumelhart, D. E., Hinton, G. E., 和 Williams, R. J. (1986). 通过反向传播错误学习表示. Nature, 323 (6088), 533–536.
[6] Duda, R. O., Hart, P. E., 和 Stork, D. G. (2001). 模式分类 (第 2 版). Wiley.
[7] Novikoff, A. B. (1962), 关于感知器收敛证明. 自动机数学理论研讨会, 12, 615–622.
[8] Rosenblatt, F. (1960). 感知器:认知系统中统计可分离性的理论 (项目 PARA 报告 60–3777). 康奈尔航空实验室.
[9] Cortes, C. 和 Vapnik, V. (1995). 支持向量网络. 机器学习, 20(3), 273–297.
[10] Bishop, C. M. (2006). 模式识别与机器学习, Springer.
[11] Rifkin, R. 和 Klautau, A. (2004). 为一对多分类辩护. 机器学习研究期刊, 5, 101–141.
[12] Minsky, M. L. (1961). 迈向人工智能的步骤. IRE 会议录, 49(1), 8–30.
[13] Horowitz, P. 和 Hill, W. (1989). 电子艺术 (第 2 版). 剑桥大学出版社.
[14] Mano, M. M. 和 Ciletti, M. D. (2007). 数字设计 (第 4 版). Prentice Hall.
[15] Merolla, P. A., Arthur, J. V., Alvarez-Icaza, R., Cassidy, A. S., Sawada, J., Akopyan, F.,... 和 Modha, D. S. (2014). 百万尖峰神经元集成电路与可扩展通信网络和接口. Science, 345 (6197), 668–673.
[16] Hastie, T., Tibshirani, R., 和 Friedman, J. (2009). 统计学习的元素:数据挖掘、推断与预测 (第 2 版). Springer.
[17] Nocedal, J. 和 Wright, S. (2006). 数值优化 (第 2 版). Springer.
[18] Schölkopf, B. 和 Smola, A. J. (2002). 使用核的学习:支持向量机、正则化、优化及其他. MIT Press.
[19] LeCun, Y., Boser, B., Denker, J. S., Henderson, D., Howard, R. E., Hubbard, W., 和 Jackel, L. D. (1989). 反向传播应用于手写邮政编码识别. 神经计算, 1(4), 541–551.
[20] Tzanetakis, G. 和 Cook, P. (2002). 音频信号的音乐类型分类. IEEE 语音与音频处理汇刊, 10(5), 293–302.
[21] Garcia-Teodoro, P., Diaz-Verdejo, J., Maciá-Fernández, G., 和 Vázquez, E. (2009). 基于异常的网络入侵检测:技术、系统和挑战. 计算机与安全, 28 (1–2), 18–28.
[22] Pang, B., Lee, L., 和 Vaithyanathan, S. (2002). 竖起大拇指?使用机器学习技术进行情感分类. ACL-02 自然语言处理经验方法会议论文集, 10, 79–86.
[23] Hornik, K., Stinchcombe, M., 和 White, H. (1989). 多层前馈网络是通用逼近器. 神经网络, 2(5), 359–366.
[24] LeCun, Y., Bengio, Y., 和 Hinton, G. (2015). 深度学习. Nature, 521 (7553), 436–444.
[25] Hochreiter, S., & Schmidhuber, J. (1997). 长期记忆。神经计算,9(8), 1735–1780。
联系方式
想要联系?请关注罗宾逊博士的LinkedIn、Twitter、Facebook和Instagram。访问我的主页获取论文、博客、邮箱注册等更多信息!
研究员与企业家,您好!作为一名研究员,罗宾逊博士提出并采用了先进的 AI 技术来理解…
日常(AI)物品的设计
构建优秀生成式 AI 工具的 UI/UX 原则
·
关注 发表在 Towards Data Science ·10 min 阅读·2023 年 10 月 6 日
--
图片来源:DreamStudio
唐·诺曼 1988 年的设计经典《日常物品的设计》阐述了用户体验原则,这些原则至今影响着卓越的硬件和软件设计。尽管诺曼举了门把手和电灯开关等类比例子,他的原则广泛适用于软件,包括生成式 AI 产品。在关于生成式 AI 的炒作中,很容易忘记即使是最先进模型驱动的产品,如果缺乏良好的 UI/UX,也会失败。
许多新的 AI 工具引起了大量兴趣,但随后用户留存表现平平(Sequoia 在这里详细说明)。AI 噱头驱动了“游客”注册,但新用户很难理解或从产品中获得真正的价值。这就是经典的“幻灭低谷”,当核心技术(生成模型)领先时,而支持技术(UI/UX 设计)滞后。
本文详细说明了如何将三个核心用户体验(UX)概念应用于生成式 AI 产品:1) 可供性,2) 反馈,和 3) 约束。 将这些概念应用于生成式 AI 可以得出一些结论,我们将探讨,包括:
-
不要追求一击即中
-
用户反馈不是免费的
-
对聊天机器人界面保持怀疑态度
以下示例来自于工作场所生产力工具(部分灵感来源于我在 Tome(一个 AI 驱动的思想塑造与分享平台)工作中的经验),但这些策略广泛适用,从开发工具到社交媒体再到电子商务。
主题 1: 反馈
向用户快速、清晰地反馈请求的操作对于任何技术都是至关重要的。对于生成式 AI 系统来说,反馈尤其重要,因为其输出具有延迟性和复杂性。反馈是双向的。系统必须从用户那里获取相关的反馈,而不至于让人厌烦,以便在短期内生成更好的输出,并在中长期内促成更好的产品版本。
以延迟为基础进行构建
生成式 AI 模型的响应时间通常在个位到十几秒之间。乍一看,等待十秒钟以获得一个引人注目的备忘录、一幅精彩的图像或一段美丽的视频似乎不是什么问题。没有生成式 AI 时,这些操作需要数小时——无论是 10 秒、1 秒还是 100 毫秒,谁在乎呢?
但用户并不是在优化机会成本的经济学家。他们已经习惯了非 AI 工具,以至于期望软件的速度快到被认为是即时的。这导致了一些用户在面对显然不即时的 AI 产品时遇到的挑战:
-
对系统是否正常工作以及是否需要重试/重新启动的困惑。
-
高感知迭代成本。而且由于 AI 生成的第一个结果通常不是用户想要的,用户往往需要进行迭代。
-
用户开始多任务处理的可能性很高。一旦用户切换离开你的应用程序,就没有保证他们会再回来。
有一些减少延迟效果的良好策略早于生成式 AI 之前就已经存在。这些包括加载动画、进度条和后台处理(用户被引导去执行其他任务,并在当前任务完成时收到通知)。一种较新的策略,特定于 LLM 功能,是逐字(或逐字符)将文本流式传输到用户界面,而不是一次性渲染完整输出。由于许多模型生成单词的速度比用户阅读的速度快,这可以将感知的延迟降低到接近零。
不要追求一杆进洞
减少延迟的一个特别有效的策略是将工作流程分解成小步骤,在每一步提供系统反馈并征求用户反馈。这使用户能够以越来越大的信心朝着输出前进,确信系统将准确地提供用户所需的内容。在设计良好的迭代工作流程中,初始步骤的延迟较低——用户对最终输出会逐步增加信任。如果你对获得所需的工件非常有信心,那么你会愿意等待十秒钟来完成最后一步。
迭代工作流程比提高延迟容忍度还有一个更强大的好处:它们使用户能够生成更符合预期的输出。生成模型有时可以从简单的用户提示中生成用户所需的内容。直接从输入到“完美”最终输出的过程是一个惊人的用户体验;这就像一杆进洞。就像一杆进洞一样,这种情况非常少见。
挑战不在于模型“聪明”到什么程度,而在于模型需要什么上下文和信息来实现用户的愿景。考虑一个希望总结其团队季度表现的销售经理。她见过数十份季度销售报告,并且对公司规范(如语气、细节水平、长度和视觉布局)非常熟悉。如果她需要同事为她撰写这样的报告,她会简单地要求“季度销售报告”,并期望同事已经了解这些规范。
所以当这位销售经理希望从 AI 工具中获得这样的报告时,她不清楚需要告诉工具什么规范,以及工具已经知道了什么。这时,迭代工作流程特别有用。她可以从一些简单且熟悉的内容开始,比如请求“季度销售报告”,然后工具可以帮助她准确地确定她的具体要求。Zach Lloyd 在这篇关于 AI 设计的合理文章中称这种模式为“提问和调整”。
Tome 的大纲编辑器是一个迭代人工智能工作流中的中间步骤的示例,位于提示和最终输出(一个多页演示文稿)之间。tome.page
用户反馈不是免费的
在许多经典的机器学习产品中,每一次用户交互都会生成一条新的训练数据,从而改善产品的下一版本。每一次用户点击搜索结果都有助于改进搜索模型。每一封用户标记为垃圾邮件的邮件都有助于改进垃圾邮件分类模型。
但许多生成性人工智能产品缺乏固有的“物理”特性,即用户交互会机械地导致模型改进。对于输出是复杂文本、图像等的人工智能产品,很难区分挫败的退出(即用户未能获得他们想要的输出而退出)与满意的退出(即用户获得了他们想要的内容而离开)。一些产品会征求用户的自愿反馈(例如点赞/点踩),但完成率往往非常低,并且反馈本身经常存在选择偏差。
设计一个工作流,其中用户的自然下一步行动可以表明他们对前一个人工智能输出的感知,这要好得多。一种模式,在文本模型中最常见,是内联建议:如果用户接受建议并继续写作,那就是一个强烈的信号,表明他们对建议持积极态度。另一种模式是记录哪些人工智能输出被保存、编辑或共享。这些与用户满意度并不完全相关——用户可能会因为图像特别怪异而分享它——但在整体使用中,它们是不错的代理指标。
主题 2:可用性
可用性是一个提示(通常是视觉的),它建议如何以及何时使用某个功能。良好的可用性使用户能够直观地与产品互动,而无需 extensive instructions or experience。我们将探索生成性人工智能在用户旅程中的三个步骤的可用性:发现人工智能的切入点、提供正确的输入给人工智能,以及使用人工智能的输出。
发现人工智能切入点
许多工作工具正在添加大量的人工智能功能,这些功能在创作过程中的不同点都适用。使用人工智能功能的高级切入点包括:
-
从头开始帮我
-
扩展我所开始的
-
编辑我所创建的内容
这些不同的切入点导致了显著不同的界面,甚至在人工智能界面发展的早期阶段就已经如此。对于(1),自由文本或“空白画布”界面已经成为早期的主流范式。对于(2),内联生成(也称为自动完成)往往主导文本生成功能(如 Github Copilot),而“给我更多类似的”则主导图像生成功能(如 Midjourney)。对于(3),界面往往集中于高亮、选择或上传现有内容(如 Grammarly)。
Whimsical 的 AI 思维导图帮助用户从头开始。 whimsical.com
对于已经发现一个 AI 入口点的用户而言,他们可能会轻易得出“这就是 AI 所在之处”的结论,而忽视其他功能。优秀的产品通过在用户工作流中最可能有用的时刻向用户介绍各种 AI 入口点来减少这一问题。
输入 AI 信息
许多生成性 AI 工作流的核心输入是自由文本输入,也称为“提示”。不幸的是,好的提示是复杂的、快速发展的,并且在工具之间不一致。好的产品帮助用户通过示例提示和工具提示来构建提示策略。
Perplexity 在其主页上包含了一些示例提示,以说明超出典型搜索引擎的使用案例。 www.perplexity.ai/
好的界面还帮助用户理解 AI 所拥有的上下文——以及它所缺乏的。当使用强大的 AI 时,合理的用户可能会得出结论,无论他们在应用中看到什么,AI 也必须能够看到和理解。例如,如果我能看到与 AI 的过去对话,AI 肯定也必须知道这一点(这是 ChatGPT 普及的行为)。但并非所有 AI 都如此工作!一些系统了解用户之前的提示,有些甚至了解比过去提示更多的上下文——而有些则只了解用户当前的交互和其他任何信息。用户不应通过反复试验来弄清楚系统知道什么和不知道什么。
使用 AI 输出
当系统生成了优秀的 AI 输出时,我们很容易认为成功已经到手。但即使输出表现良好,对于用户来说,这可能也是一个令人困惑的时刻。
首先,新用户经常不知道如何保存输出。即使输出很不错,许多用户仍然希望进行迭代,看看是否能从好变得更好。但害怕丢失现有工作可能会导致犹豫和挫折感。
其次,用户可能会困惑于如何改进输出。假设他们使用了“从头开始”的 AI 功能,他们是否应该回到最开始?他们是否需要转到不同的 AI 入口点,如“扩展”或“编辑”?许多用户可能遇到过像 ChatGPT 这样的产品,其中输出不是直接可编辑的;如果输出是可编辑的,用户可能需要一个编辑工具。
主题 3:约束
约束限制输入和输出,以帮助用户更快更好地工作。好的约束对用户来说是明确的。如果系统可以帮助用户实现目标——但仅仅是部分实现或部分时间——那么最好完全阻止这条路径,而不是提供一个不可靠的体验。
大型语言模型开辟了广阔的新用户体验(这就是我喜欢从事这项工作的原因!),产品创造者应热衷于放松来自确定性软件的传统约束。尽管如此,无论大型语言模型变得多么智能,总会有一些深思熟虑的约束存在的空间。
输入:不要害怕控制
受到 ChatGPT 成功的启发,许多生成型人工智能工具将自由文本框作为唯一或主要的用户输入。但用户的意图的许多方面通过分类或数字输入更能得到有效表达。在创建文档时,大多数用户会考虑语言(一个分类)和长度(一个数值)。用户可能不会在自由文本提示中提及这些属性,但这并不意味着他们不在意。通过离散的、有界的控制(如下拉菜单或滑块)来征询这些输入,系统帮助获取所需的输入,以提供用户脑海中的内容。对于帮助用户导航离散控制,有一些经久不衰的原则:设置良好的默认值,逻辑地分组控制,并通过工具提示或标签解释控制。
关于控制,设置良好的默认值是设计的关键部分。在绝大多数情况下(远远超过 90%),用户不会更改默认设置,即使这样做对他们有利。将良好的默认值与用户偏好的变异结合的一个机会是通过硬编码规则或人工智能动态调整默认值。
输出:并非所有可以生成的内容都应该生成
对于生成型人工智能产品,有许多情况是基础模型可以生成某些内容,但用户宁愿什么也不做,也不愿意应对误导性或刺耳的输出。
对于大多数工作相关任务,用户宁愿选择“我不知道”也不愿接受需要验证或反驳的潜在错误答案。这项哈佛大学在咨询公司 BCG 进行的研究展示了当人工智能回答超出其“信心边界”的问题时,如何影响工作质量,而用户对边界并不知情,因此没有充分审查输出。
减少幻觉的方法正在迅速发展(例如,检索增强生成),我怀疑幻觉将在几年后成为一个“解决”的问题——但今天,当事实准确性至关重要时,输出仍然是一个重要的考虑约束的地方。
法律和伦理问题是限制用户面对输出的第二个原因。仅仅因为底层模型可以生成某个主题的文本或图像,并不意味着这样做是合乎良心的。然而,许多情况下,当系统将用户请求分类为“超出范围”时,用户的意图实际上是善意的。通过一点帮助,用户可以重新表述他们的请求,以保持在范围内。例如,一些图像生成工具拒绝包含“child”一词的提示。但如果用户想生成一个有孩子的家庭的图像,他们可以提示“家庭四口”或“父母和儿子女儿”。关键在于,限制对用户来说是明确的。
随着生成式 AI 产品的流行,优秀的产品设计师和产品经理会记住:成功不仅仅源于 AI 的智能程度,还在于产品如何引导用户通过 AI 驱动的工作流程。核心设计概念如反馈、可用性和限制依然重要,但它们的实施战术和模式正在迅速演变。善用这些设计模式对任何希望超越初期炒作周期并提供持久、广泛使用产品的 AI 公司至关重要。
监控生产环境中的机器学习模型的难点
·发表于 Towards Data Science ·7 分钟阅读·2023 年 3 月 14 日
--
照片由 Luke Chesser 提供,来自 Unsplash
成为数据科学家可能听起来像是一份简单的工作 — 准备数据、训练模型并将其部署到生产环境中。然而,现实却远非如此。这个工作更像是照顾一个婴儿 — 一个永无止境的监控周期,确保一切正常。
挑战在于需要关注三个关键组件:代码、数据和模型本身。每个元素都有其自身的难点,使得在生产环境中的监控变得困难。
在这篇文章中,我们将深入探讨这些挑战,并提及可能的解决方法。
模型失败的两种方式
模型未能进行预测
代码中的错误示例。图像由作者提供。图像由作者提供。
当我们谈论模型未能进行预测时,意味着它无法生成输出。由于模型始终是更大软件系统的一部分,它还会面临更多技术挑战。以下是一些可能的例子:
-
语言障碍 — 将一个用一种编程语言构建的模型集成到用另一种语言编写的系统中。这可能需要额外的代码或“粘合”代码来连接两种语言,增加了复杂性和失败的风险。
-
维护代码 — 正如我们所知,库和其他依赖项不断更新,因此它们的功能命令等也会变化。跟踪这些变化与代码的关系很重要,以确保代码不会过时。
-
扩展问题 — 随着我们的模型用户越来越多,基础设施可能无法足够强大以处理所有请求。
良好的软件监控和维护系统应能防止可能的问题。即使发生了问题,我们也会直接获得异常信息。在下一节中,我们将深入探讨那些检测不那么明显的问题。
模型的预测失败
在这种情况下,模型生成了输出,但其性能正在下降。这种失败特别棘手,因为它通常是无声的——没有明显的警报或指示器表明发生了什么。整个管道或应用程序可能看起来运作正常,但模型生成的预测可能已不再有效。
这个问题有以下两个原因:
协变量漂移
输入特征的分布随时间变化。
训练和生产数据中 CLV 预测的协变量漂移分布。作者提供的图片。
上图展示了一个社交媒体平台上客户生命周期价值 (CLV) 预测的假设场景。训练数据的分布严重偏向年轻客户。然而,当模型在生产中部署时,分布发生了变化,年长客户略有增加。
一个可能的解释是,当训练数据被收集时,平台的大多数用户是年轻的。随着平台的流行,年长的客户开始注册。这个新用户群体的特征与历史用户不同,这意味着模型现在更容易出错。
数据漂移的检测相对简单。我们需要比较不同时间段的分布,以识别数据的变化。棘手的部分是,并非所有漂移都会导致性能下降。
有时,数据的变化可能不会影响模型的整体性能。这是因为并非每个输入特征对输出的贡献都是一样的。只有重要特征的变化会对模型的整体性能产生显著影响。
概念漂移
模型输入和输出之间的关系发生了变化。
训练和生产数据中 CLV 与“年龄”特征的关系。作者提供的图片。
让我们回到客户生命周期价值预测的例子。如我们所见,年轻客户的 CLV 在生产中有所下降。这可能是由于年轻用户迁移到其他社交媒体平台造成的,比如我们看到的从 Facebook 到 Instagram 的快速转移。在训练时提取的年龄与 CLV 的关系在生产中已不再相关。
与协方差转移不同,概念漂移几乎总是影响模型的商业影响。更具挑战性的是,概念漂移并不容易检测。一个可能的解决方案是对标签进行相关性分析,或在分析和参考期训练并比较两个独立模型。另一种方法是仔细监控模型性能随时间的变化。如果性能下降,这可能表明概念漂移正在发生。
然而,监控模型性能并非总是容易的,尤其是当目标标签的访问受到限制时。这是一个关键挑战,我们将在下一段中更详细地探讨。
真实值的可用性
如前所述,访问目标值是监控生产中机器学习模型的关键方面。根据这些值的可用性,我们可以区分三种类型:
即时
真实值是立即可用的。
一个典型的即时可用性例子是汽车到达预测。在旅行完成后,我们可以立即评估预测结果,并获得模型的实时性能。
使用 NannyML 进行即时真实值的性能监控。图片由作者提供。
上图展示了分类问题中的一种常见场景,其中参考期代表测试数据集,分析期代表来自生产的实时数据流。在右侧,我们可以看到一个表格数据的例子,并且突出显示了目标值。通过即时访问真实值,我们可以不断监控模型的性能。如果性能低于某个阈值,如从六月到十月所示,这表明我们需要调查下降的原因。
对目标值的即时访问使得监控和评估模型性能变得更加容易。然而,世界是复杂的,获取即时真实值并非总是那么简单。
延迟
真实值在时间上被推迟。
对服装公司的需求预测是一个很好的例子,说明真实值的延迟。这些公司使用机器学习模型预测下一个季节的需求。然而,评估预测结果是一项棘手的任务,因为他们必须等待 3 个月,直到季节结束才能衡量预测的准确性。
使用 NannyML 进行带有延迟真实值的性能监控。图片由作者提供。
正如右侧所示,我们的表格数据由于延迟而缺少关键的目标值。这种信息缺失在图表中体现出来,我们可以看到从五月到十一月模型的 ROC AUC 性能存在一个间隙。这使得实时了解模型的表现及其决策是否仍然准确变得具有挑战性。
缺失
完全没有真实数据。
在某些情况下,例如完全自动化的流程中,可能无法获得真实数据。一个例子是使用机器学习模型进行保险定价。这些模型在生产环境中部署,根据人口统计信息或车辆信息预测保险价格。然而,由于这个过程是自动化的,没有人参与评估预测的准确性。
使用 NannyML 在缺乏真实数据的情况下进行性能监控。
当我们查看表格数据时,我们可以看到目标值完全缺失。这在我们的性能图中反映出来,部署后图表是空白的。这使得很难清楚地了解我们的模型实际表现如何,以及其预测是否仍然可靠。
尽管缺乏真实数据听起来可能令人沮丧,但这并不意味着模型的性能无法评估。即使模型没有目标值,它仍然会生成输出,我们可以将其与之前的数据进行比较。这些信息足以估计其性能。虽然真实数据的可用性使得评估模型性能的任务更容易,但即使在其缺失的情况下,也可以进行评估。
结论
作为数据科学家,关键责任之一是确保整个模型和管道顺利运行。然而,这通常是一个具有挑战性的任务,因为在生产过程中可能会出现几个障碍,例如:
-
代码 — 模型因错误无法进行预测
-
数据 — 协变量偏移、概念漂移或对真实数据的有限访问
但不用担心;现在你知道了需要注意的事项以及这些问题发生的原因,你将能够轻松应对它们。
祝监控愉快!
这篇博客最初发布于 https://www.nannyml.com/blog
ETL 的 Docker Compose:Meerschaum Compose
图片由CHUTTERSNAP拍摄,刊登于Unsplash
·
关注 发表在Towards Data Science · 6 分钟阅读 · 2023 年 6 月 19 日
--
本文介绍了Meerschaum Compose,这是一个用于在 YAML 中定义 ETL 流水线的工具,以及一个数据工程框架Meerschaum的插件。
Docker 的出现彻底改变了我们设计、构建和运行云应用程序的方式。然而,开发者很快意识到其灵活性使得协作变得困难,因此 docker-compose
成为管理环境和多容器项目的首选工具。
类似地,这个一致环境的问题也出现在 ETL 框架 Meerschaum 中。随着越来越多的数据工程师使用该平台构建他们的管道,管道的动态特性意味着需要一个解决方案来提供项目级的隔离。
受到 Docker Compose 的启发,这个解决方案以一个名为 Meerschaum Compose 的项目形式出现。我每天在工作和个人项目中使用 Compose 来构建和管理数据管道,今天我想展示如何使用 Compose 构建 ETL 项目。
Compose 如何驯服引擎
权力越大,责任越大。在 Docker 的情况下,这种责任由一个名为docker-compose.yml
的清单文件处理,该文件描述了应用程序内的服务应该如何运行。该文件充当了活文档,便于原型设计并向 CloudOps 描述预期环境。通过一个简单的标准,Docker Compose 通过提供一种方便的方式来标准化和共享多服务项目的环境,从而填补了开发过程中的空白。
Meerschaum Compose 服务于类似的目的:在mrsm-compose.yml
文件中,你可以指定项目可能需要的一切:预期的 环境、插件、管道和 连接器。
命令
当你遇到一个新的 Compose 项目(在下面的屏幕截图中使用 Tech Slam ‘N Eggs 演示项目)时,尝试这些命令来熟悉环境:
注意: 如果你想在 Docker 容器中运行 Compose,请参见 这个仓库。
**mrsm compose run**
注册管道并逐一同步它们。这对于确保执行顺序以及使用数据库更新管道参数非常有用。一个常见的模式是将不同阶段的 Compose 文件链接在一起,作为更大的 ETL 过程的一部分:
mrsm compose run --file mrsm-compose-00-extract.yaml && \
mrsm compose run --file mrsm-compose-01-transform.yaml && \
mrsm compose run --file mrsm-compose-02-load.yaml
注意: 命令
mrsm compose sync pipes
是并行执行的,最适用于 Compose 文件中的管道彼此独立的情况。
Compose 运行命令一次同步一个管道。
**mrsm compose explain**
解析 Compose 文件并打印定义管道的当前环境和状态。这在故障排除和理解项目结构时非常有用。
Compose explain 命令打印定义管道的状态。
**mrsm compose up --dry**
注册并更新远程管道的参数。这隐含地作为mrsm compose run
的一部分运行,并应在标准的 Meerschaum 操作(如mrsm compose sync pipes
)之前运行。--dry
标志可以防止同步作业运行。
注意: Compose 将 标记管道 与项目名称。如果在一个项目中使用多个 compose 文件,请设置键
project_name
。
**mrsm compose down -v**
停止作业并删除管道。这类似于 docker compose down -v
(即 -v
表示“卷”)。
**mrsm compose <action>**
从项目环境中执行标准的 Meerschaum 操作(例如 sync pipes
、delete pipes
、自定义操作)。我经常运行 mrsm compose python
以跳入项目环境中的 REPL。
每当你运行 mrsm compose
命令时,标志 --tags {project_name}
会被附加(除非被覆盖),以确保你只与项目中的管道进行交互。
一个 Meerschaum Compose 项目的示例,用于天气数据的 ETL。
管道
谦逊的 管道 是 Meerschaum 对增量 ETL 的抽象。管道具有输入和输出 连接器 并存储 参数 来配置其同步过程的行为。这可以是简单的 SQL 查询,也可以包括用于插件的自定义键。
上述 Compose 项目的管道通过 web UI 展示
由于管道的元数据与其表一起存储,它们易于编辑(无论是通过 edit pipes
还是在 web UI 上),这有助于原型设计。但这种动态特性引入了本文开头描述的同样问题:为了扩展开发,需要一个 Compose 文件来定义项目组件,以便于版本控制。
根据 Meerschaum Compose 规范,管道在 sync:pipes
键下的列表中定义。每个项目定义了构建管道所需的键和参数,就像你期望数据库中的管道所反映的蓝图。
例如,以下代码片段将定义一个管道,该管道会将一个 weather
表从远程 PostgreSQL 数据库(在下面定义为 sql:source
)同步到本地 SQLite 文件(在本项目中定义为 sql:dest
)。
sync:
pipes:
- connector: "sql:source"
metric: "weather"
target: "weather"
columns:
datetime: "timestamp"
station: "station"
parameters:
fetch:
backtrack_minutes: 1440
query: |-
SELECT timestamp, station, temperature
FROM weather
config:
meerschaum:
instance: "sql:dest"
connectors:
sql:
source: "postgresql://user:pass@host:5432/db"
dest: "sqlite:////tmp/dest.db"
这个示例将使用日期时间轴 timestamp
进行范围边界(向回追溯 1 天)逐步更新一个名为 weather
的表,这一列加上 ID 列 station
一起构成一个复合主键,用于去重。
URI 是字面上写的示例;如果你正在提交一个 compose 文件,请引用环境变量(例如
$SECRET_URI
)或你的主 Meerschaum 配置(例如MRSM{meerschaum:connectors:sql:source}
)。
连接器
首先,简单回顾一下Meerschaum 连接器:你可以通过几种方式定义连接器,其中最受欢迎的是通过环境变量。假设你在环境文件中定义了连接秘密:
export MRSM_SQL_REMOTE='postgresql://user:pass@host:5432/db'
export MRSM_FOO_BAR='{
"user": "abc",
"password": "def"
}'
第一个环境变量MRSM_SQL_REMOTE
定义了连接器sql:remote
。如果你引入了这个文件,你可以通过命令mrsm show connectors sql:remote
来验证这个连接器。
第二个变量是如何定义自定义FooConnector
的示例,你可以通过插件中的@make_connector
装饰器来创建它。自定义连接器是一个强大的工具,但现在,这里是基本结构:
from meerschaum.connectors import make_connector, Connector
@make_connector
class FooConnector(Connector):
REQUIRED_ATTRIBUTES = ['username', 'password']
def fetch(pipe, **kwargs):
docs = []
return docs
所以我们刚刚回顾了如何在主机环境中定义连接器。让我们看看如何在 Meerschaum 项目中使这些主机连接器可用。在 Compose 文件中,我们项目所需的所有连接器都在config:meerschaum:connectors
下定义。使用MRSM{}
语法来引用主机环境中的键,并将它们传递到项目中。
config:
meerschaum:
instance: "sql:app"
connectors:
sql:
app: MRSM{meerschaum:connectors:sql:remote}
foo:
bar: MRSM{meerschaum:connectors:foo:bar}
插件
Meerschaum 可以通过插件进行轻松扩展,这些插件是 Python 模块。插件可以获取数据、实现自定义连接器和/或扩展 Meerschaum(例如,自定义操作、标志、API 端点等)。
Meerschaum 支持多个插件目录(通过
MRSM_PLUGINS_DIR
),可以在mrsm-compose.yaml
中的plugins_dir
键下设置(默认是plugins
目录)。
将你的插件存储在 Compose 项目中,可以清楚地说明你期望插件的使用方式。例如,MongoDBConnector 项目中的 Compose 文件演示了自定义连接器如何同时作为连接器和实例使用。
包管理
当你第一次开始使用 Meerschaum Compose 时,首先会注意到它会开始安装大量的 Python 包。不要担心你的环境——所有东西都安装在你项目的root
子目录中的虚拟环境中(有点讽刺,对吧?)。你可以用mrsm compose init
安装插件的依赖项。
为了在项目之间共享包,请在mrsm-compose.yml
中设置root_dir
键为新的路径。删除这个root
目录将有效卸载 Compose 下载的所有包,同时保持你的主机环境不变。
结论
Meerschaum Compose 已经成为我构建中型 ETL 项目的首选工具。它的工作流程类似于Meltano或dbt,但入门门槛更低,对 ETL 过程的控制更为动态。这是一种组织插件、连接器和管道的整洁工作流程,非常适合团队合作。
你可以通过Meerschaum Compose 模板仓库快速启动一个新项目——查看MongoDBConnector 插件或Tech Slam ‘N Eggs 演示以获取实际示例。
随意将你的项目添加到Awesome Meerschaum 列表!
恐怖的对手:机器学习中的数据泄漏
可能是机器学习中最被低估的概念之一
·
关注 发表在 Towards Data Science · 13 分钟阅读 · 2023 年 5 月 19 日
--
我参加了超过 5 门商务分析和机器学习课程,包括面对面课程和在线课程。令人惊讶的是,只有一门课程稍微触及了数据泄漏的话题。
图片由 Luis Tosta 提供,来源于 Unsplash
讨论数据泄漏时,在没有机器学习背景的情况下,我们通常指的是机密信息在没有适当安全措施或许可的情况下转移到第三方,导致隐私和安全的泄露¹。
尽管这个概念有些类似,但在机器学习的背景下并不完全是这样。这在机器学习领域的含义是:
数据泄漏发生在测试数据集的信息错误地被包含在训练数据集中。²
结果是什么? 训练过程中性能指标表现得不切实际地好,但当模型真正投入使用时表现很差。
简单来说, 模型记住了它不该访问的信息,导致训练过程中性能指标被人为地夸大。
还是无法理解? 好吧,想象一下。你正在为即将到来的数学考试复习。你做了很多练习题以便每天进步。然后,你发现考试题目不小心泄露到了网上。你获得了这些关键信息,并决定在这张试卷上练习(你在这个考试前不应知道的数据集上训练自己,因此,你“记住”了题目模式)。结果?你对这张试卷变得过于熟悉,表现指标异常优秀,但当你真的投入到实际情况中时……(我们还是不提了)。
目录
-
机器学习中的数据泄漏类型(目标泄漏,训练-测试污染,数据预处理中的泄漏)
-
机器学习中数据泄漏的后果
-
预防机器学习中的数据泄漏(手动审查,数据清理和预处理,使用管道,适当的验证技术)
-
现实世界数据集示例:泰坦尼克号数据集
-
数据泄漏第一种示例:将目标存活作为特征包含
-
数据泄漏第二种示例:混淆训练和测试数据的记录
-
数据泄漏第三种示例:错误的数据预处理步骤
-
数据泄漏第四种示例:将特征舱位包含为模型中的一部分
目标泄漏
关于目标泄漏,识别可能不会那么简单明了。想象一下:你正在构建一个模型来预测客户是否会取消他们的月度订阅服务(即流失)。乍一看,将“客户服务电话的数量”作为特征包含在模型中似乎并没有问题,因为你可能认为更多的客户服务电话与更高的流失概率相关。
然而,经过仔细检查发现,“客户服务电话数量” 是客户流失的结果,而不是一个贡献特征。已经决定流失的客户只是打电话解决未结清的问题,然后最终取消订阅。因此,在预测客户是否会流失时,这些信息是不可用的(换句话说,我们只知道已经决定流失的客户的信息)。
将目标变量作为特征变量的一部分,或任何直接或间接从目标变量派生的代理变量,可能导致数据泄漏。
训练-测试污染和数据预处理中的泄漏
这些情况指的是对训练集和测试集应用相同的预处理步骤。例如,当我们进行特征缩放、估计缺失值和去除异常值等数据预处理步骤时,我们应该确保不会像下面所示的那样从测试数据集中“学习”³。
scaler = StandardScaler()
scaler.fit(X_train)
scaler.transform(X_train)
scaler.transform(X_test)
在这里,我们在预处理步骤之前将数据集分成训练集和测试集,以便我们只能拟合训练数据集。请注意,我们不应该在整个数据集(包括训练集和测试集)上拟合,因为这会导致数据泄漏(我们的模型学会了它不应该学习的东西,换句话说,在预测时对测试数据集的信息是不知道的)。
在训练数据上拟合,在训练和测试数据上转换。
机器学习中数据泄漏的后果
在机器学习项目中未能检测到数据泄漏的后果是巨大的——它们带来了虚假的希望。有没有遇到过训练性能极高而测试数据集表现非常差的情况?数据泄漏可能是罪魁祸首。这里的关键字是过拟合和无法泛化。这是因为模型学会了记忆噪声和无关信息,导致在面对真实测试数据集时表现不佳²。
最终结果?
你会做出不准确的模型评估和不可靠的预测。真是资源浪费!
防止数据泄漏:手动审查
是的,我们都明白。手动审核效率低下且非常耗时。然而,花时间研究特征与目标变量之间的关系,也许是检测数据泄漏最一致的方式,因此,非常值得。当一个特征与目标变量的相关性非常高时,我们应该怀疑并进一步调查这种关系。有时,进行探索性数据分析(EDA)可能有助于揭示特征与目标之间的相关性。此外,全面的领域知识和专业技能可以帮助确定一个特征是否应该包含在模型中。记住,当有疑问时,总是要问自己这个指导性问题。
“这个特征是否包含在预测时不可用的信息?”
如果上述问题的答案是“是”,那么包括该特征可能会导致数据泄漏。
防止数据泄漏:管道是王道
我之前参加的商业分析和机器学习课程中,没有提到构建机器学习预处理管道。最常见的做法是编写到处都是的“意大利面条”代码,而没有任何工作流程标准化。虽然这对很多人来说可能很熟悉,但这并不是最佳实践——其中一个原因是可能会将数据泄漏引入模型。我第一次接触到利用管道的想法是来自于书籍Data Cleaning and Exploration with Machine Learning⁴。从这本书中,我学到的关键经验包括通过将每个预处理步骤作为变量参数嵌入到make_pipeline
方法中,并分别为数值型、分类变量和二进制变量分开处理。
简单来说,管道是一系列线性的数据预处理步骤,依次执行。管道提供了一个清晰有序的链式过程,用于自动化机器学习项目的工作流程。我们可以利用 scikit-learn 的 Pipeline 类⁵,它接受一个元组列表作为输入,每个元组代表管道中的一个步骤。每个元组的第一个元素是表示步骤名称的字符串,第二个元素是 scikit-learn 转换器或估计器对象的实例。当然,还有一个简化的方法,就是**make_pipeline**
,它不要求我们命名估计器(我们都是懒惰的生物)。记住,估计器需要具有**fit**
和**transform**
方法。
为什么要使用 Pipeline?
管道自动处理所有数据处理过程。它还确保每个步骤仅在训练数据上进行拟合,从而防止数据泄漏,并确保各阶段按正确顺序进行。
这里有一个示例:我们需要对数据集中不同类型的数据进行数据预处理,即数值型、分类型和二元特征,每种特征都有不同的步骤。我们可以利用**make_pipeline**
将过程按顺序安排,让 Pipeline 处理所有后台工作。这将返回一个**Pipeline**
对象,该对象具有多个属性和方法可供调用。例如,我们可以调用**fit**(X_train, y_train)
和**score**(X_test, y_test)
来分别拟合和评估模型。
from sklearn.preprocessing import StandardScaler
from sklearn.impute import SimpleImputer
from sklearn.pipeline import make_pipeline
from feature_engine.encoding import OneHotEncoder
from preprocfunc import OutlierTrans #self-created Python class
standardtrans = make_pipeline(OutlierTrans(2),
StandardScaler()
)
categoricaltrans = make_pipeline(SimpleImputer(strategy="most_frequent"),
OneHotEncoder(drop_last=True)
)
binarytrans = make_pipeline(SimpleImputer(strategy="most_frequent")
)
columntrans = ColumnTransformer(transformers=[
("standard", standardtrans, numerical_cols),
("categorical", categoricaltrans, ['gender']),
("binary", binarytrans, ['completedba'])
])
lr = LinearRegression()
pipe = make_pipeline(columntrans, KNNImputer(n_neighbors=5), lr)
防止数据泄漏:交叉验证
本节内容受到《数据清洗与机器学习探索》⁴一书的启发。我从书中获得的另一个观点是将 Pipeline 和交叉验证的概念结合起来。是的,它们并不是互相排斥的!训练和测试数据集的选择非常关键,如果做得不对可能会导致数据泄漏。当我们不执行交叉验证来评估模型时,我们面临着对训练数据过拟合以及在新的、未见过的数据上表现不佳的风险。我们进行的一次性训练测试拆分可能使模型学习到某个特定特征,这个特征可能对该拆分是独有的,而不具有普遍性。
为什么要进行交叉验证?
交叉验证使我们能够更精确地预测模型在全新、未测试数据上的表现。通过使用交叉验证,我们可以在多个数据子集上测试模型的有效性。
我们可以利用scikit-learn的 K 折交叉验证来实现这一点。
K 折交叉验证的工作原理是什么?数据首先被分成k个大小相等的折叠,然后在k-1个折叠上训练模型,再在最后一个折叠上测试模型。在这个过程中,每个折叠都作为一次测试集,这个过程重复进行 k 次。在迭代结束时,通过对 k 次迭代结果的平均来估算模型的性能。当 k 设置为 1 时,这意味着我们回退到通常的训练测试拆分。我们在整个数据集上训练模型,并在一个独立的数据集上进行测试。
好消息是,我们可以从 Pipeline 中未完成的地方继续进行。
from sklearn.model_selection import cross_validate, KFold
ttr = TransformedTargetRegressor(regressor=pipe, transformer=StandardScaler())
kf = KFold(n_splits=5, shuffle=True, random_state=0)
scores = cross_validate(ttr,
X=X_train,
y=y_train,
cv=kf,
scoring=('r2', 'neg_mean_absolute_error'),
n_jobs=1)
真实世界数据集示例:泰坦尼克号数据集
泰坦尼克号。经典。泰坦尼克号数据集是经典的机器学习问题,我们为每个乘客提供了一组特征,例如他们的年龄、性别、票务等级、登船地点,以及他们是否有家人在船上⁶。使用这些特征,目标是训练一个机器学习模型来预测乘客是否**幸存**
。以下是一个简短的预测版本,未涉及超参数调整和特征选择。
使用以下代码清理原始数据集。
import numpy as np
import pandas as pd
from sklearn.impute import SimpleImputer
df = pd.read_csv("../dataset/titanic/csv_result-phpMYEkMl.csv")
#Change column names, replace "?" to "NaN", change data types
def tweak_df(df):
features = ["PassengerId", "Survived", "Pclass", "Name", "Sex", "Age", "SibSp", "Parch", "Ticket", "Fare", "Cabin", "Embarked"]
return (df
.rename(columns={"id": "PassengerId", "'pclass'": "Pclass", "'survived'": "Survived", "'name'": "Name", "'sex'": "Sex", "'age'": "Age", "'sibsp'": "SibSp", "'parch'": "Parch", "'ticket'": "Ticket", "'fare'": "Fare", "'cabin'": "Cabin", "'embarked'": "Embarked"})
[features]
.replace('?', np.nan)
.astype({'Age': 'float', 'Fare': 'float16'})
)
#Splitting dataset into train, validation, test, and unseen
X_train_val_test, X_unseen, y_train_val_test, y_unseen = train_test_split(tweak_df(df).drop(columns=['Survived']), tweak_df(df).Survived, test_size=0.33, random_state=42)
X_train, X_val_test, y_train, y_val_test = train_test_split(X_train_val_test, y_train_val_test, test_size=0.4, random_state=42)
X_val, X_test, y_val, y_test = train_test_split(X_val_test, y_val_test, test_size=0.5, random_state=42)
#Extensive cleanup
def tweak_titanic_cleaned(train_df):
impute_table = (train_df
.assign(SibSp=lambda df_: np.where(df_.SibSp==0, 0, 1),
Parch=lambda df_: np.where(df_.Parch==0, 0, 1))
.groupby(['SibSp', 'Parch'])
['Age']
.agg('mean')
)
train_df_intermediary = (train_df
.assign(SibSp=lambda df_: np.where(df_.SibSp==0, 0, 1),
Parch=lambda df_: np.where(df_.Parch==0, 0, 1),)
)
condlist = [((train_df_intermediary.Age.isna()) & (train_df_intermediary.SibSp == 0) & (train_df_intermediary.Parch == 0)),
((train_df_intermediary.Age.isna()) & (train_df_intermediary.SibSp == 0) & (train_df_intermediary.Parch == 1)),
((train_df_intermediary.Age.isna()) & (train_df_intermediary.SibSp == 1) & (train_df_intermediary.Parch == 0)),
((train_df_intermediary.Age.isna()) & (train_df_intermediary.SibSp == 1) & (train_df_intermediary.Parch == 1)),]
choicelist = [impute_table.iloc[0],
impute_table.iloc[1],
impute_table.iloc[2],
impute_table.iloc[3],]
bins = [0, 12, 18, 30, 50, 100]
labels = ['Child', 'Teenager', 'Young Adult', 'Adult', 'Senior']
features = ["Survived", "Pclass","Sex","Fare","Embarked","AgeGroup","SibSp","Parch","IsAlone","Title"]
return (train_df
.assign(Embarked=lambda df_: SimpleImputer(strategy="most_frequent").fit_transform(df_.Embarked.values.reshape(-1,1)),
Age=lambda df_: np.select(condlist, choicelist, df_.Age),
IsAlone=lambda df_: np.where(df_.SibSp + df_.Parch > 0, 0, 1),
Title=lambda df_: df_.Name.str.extract(',(.*?)\.'))
.assign(AgeGroup=lambda df_: pd.cut(df_.Age, bins=bins, labels=labels),
Title=lambda df_: df_.Title.replace(['Dr', 'Rev', 'Major', 'Col', 'Capt', 'Sir', 'Lady', 'Don', 'Jonkheer', 'Countess', 'Mme', 'Ms', 'Mlle','the Countess'],
'Other'))
.set_index("PassengerId")
[features]
)
让我解释一下我所实现的数据预处理步骤的理念:
-
使用 scikit-learn 的
SimpleImputer
类用最频繁的条目填补Embarked
列中的缺失数据。 -
使用基于乘客是否有家人随行的条件的均值列表来填补
Age
列中的缺失数据。(即,如果乘客没有家人随行,则我们用其他没有家人随行的乘客的均值进行填补) -
创建了一个特征
IsAlone
用于表示乘客是否有家人随行 -
创建了一个特征
Title
用于表示乘客的头衔 -
创建了一个特征
AgeGroup
用于将乘客划分为 5 个不同的年龄组。
数据泄漏第 1 种示例:将目标 **survived**
作为特征
#Intentionally add target variable to list of features
X_train = tweak_titanic_cleaned(pd
.concat([X_train, pd.DataFrame(y_train)], axis=1))
X_val = tweak_titanic_cleaned(pd
.concat([X_val, pd.DataFrame(y_val)], axis=1))
X_test = tweak_titanic_cleaned(pd
.concat([X_test, pd.DataFrame(y_test)], axis=1))
# Prepare the training data
X_train = pd.get_dummies(X_train, columns=["Survived", "Pclass", "Sex", "Embarked", "AgeGroup", "IsAlone", "Title"], drop_first=True)
X_val = pd.get_dummies(X_val, columns=["Survived", "Pclass", "Sex", "Embarked", "AgeGroup", "IsAlone", "Title"], drop_first=True)
# Scale numerical columns
scaler = MinMaxScaler()
num_cols = ["Fare","SibSp","Parch"]
X_train[num_cols] = scaler.fit_transform(X_train[num_cols])
X_val[num_cols] = scaler.transform(X_val[num_cols])
# Fit and evaluate Logistic Regression model
lr_model = LogisticRegression(random_state=0)
lr_model.fit(X_train, y_train)
# Make predictions on validation data
y_pred_val = lr_model.predict(X_val)
# Evaluate model on validation data
acc_val = round(accuracy_score(y_val, y_pred_val) * 100, 2)
print("Logistic Regression Model accuracy on validation data:", acc_val)
正如你可能预期的那样,将目标变量 survived
作为特征有效地使我们的模型变得毫无用处,因为它在验证数据上的准确率现在是 100.0%。进行预测没有意义。这种错误很容易发现,但不常见。
数据泄漏第 2 种示例:混淆训练数据和测试数据的记录
如果测试数据被意外地包含在训练集中,模型可能会在这些泄漏的信息上进行训练,从而在测试集上表现得不切实际。以下是一个故意编造的例子,展示了如何将部分测试集包含到训练集中。
X_train = (pd
.concat([tweak_titanic_cleaned(X_train),
tweak_titanic_cleaned(X_val).iloc[:150, :]])
)
y_train = (pd
.concat([y_train,
y_val.iloc[:150]])
)
X_val = tweak_titanic_cleaned(X_val)
# Prepare the training data
X_train = pd.get_dummies(X_train, columns=["Pclass", "Sex", "Embarked", "AgeGroup", "IsAlone", "Title"], drop_first=True)
X_val = pd.get_dummies(X_val, columns=["Pclass", "Sex", "Embarked", "AgeGroup", "IsAlone", "Title"], drop_first=True)
# Scale numerical columns
scaler = MinMaxScaler()
num_cols = ["Fare","SibSp","Parch"]
X_train[num_cols] = scaler.fit_transform(X_train[num_cols])
X_val[num_cols] = scaler.transform(X_val[num_cols])
# Fit and evaluate Logistic Regression model
lr_model = LogisticRegression(random_state=0)
lr_model.fit(X_train, y_train)
# Make predictions on validation data
y_pred_val = lr_model.predict(X_val)
# Evaluate model on validation data
acc_val = round(accuracy_score(y_val, y_pred_val) * 100, 2)
print("Logistic Regression Model accuracy on validation data:", acc_val)
数据泄漏第 3 种示例:错误的数据预处理步骤
在这里,我们应该在预处理步骤之前将数据集分成训练集和测试集。如果我们在拆分数据集之前进行预处理步骤,我们可能会意外地从测试数据集中学习,从而导致模型性能被过度夸大。
# Prepare the training data
df_leaked = pd.get_dummies(tweak_titanic_cleaned(X_train_val_test), columns=["Pclass", "Sex", "Embarked", "AgeGroup", "IsAlone", "Title"], drop_first=True)
# Scale numerical columns
scaler = MinMaxScaler()
num_cols = ["Fare","SibSp","Parch"]
df_leaked[num_cols] = scaler.fit_transform(df_leaked[num_cols])
# Split the data into train, validation, and test sets
X_train, X_val_test, y_train, y_val_test = train_test_split(df_leaked, y_train_val_test, test_size=0.4, random_state=42)
X_val, X_test, y_val, y_test = train_test_split(X_val_test, y_val_test, test_size=0.5, random_state=42)
正确的步骤是对训练数据集进行 fit_transform
,对测试数据集进行 transform
。
数据泄漏第 4 种示例:将舱位特征作为模型的一部分
这个数据泄漏问题不容易发现,可能需要一些领域知识来理解。主要问题是“这个特征是否包含在预测时不可用的信息?” 如果答案是肯定的,那么很有可能存在数据泄漏。
在这种情况下,“预测”是在乘客已经登船且事件已经发生后进行的。目标是基于事件发生后的现有数据(即乘客等级、年龄等)预测乘客是否会生存。在预测时,舱位号信息不可用,因为它仅在乘客登船后才分配给乘客。
不是每位乘客的舱号在数据集中都有记录,实际上,我们有大量缺失的数据。即使对于那些有记录的乘客,舱号也可能不准确或不完整。因此,我们在开发估算生存模型时不能将舱号作为预测因子,因为它可能并不对所有乘客都准确或可用。
想象一下,我们决定将舱号作为模型中的一个预测因子。在训练模型时,它使用舱号进行预测,并且表现得非常好。但是,当我们尝试在现实世界中使用模型时,我们可能没有所有乘客的舱号,或者我们拥有的舱号可能是错误的。这意味着即使模型在训练过程中非常准确,它在实际应用中可能表现不好。
一个可能的解决方案是删除这个特征,并在模型构建中排除它。
后记
防止数据泄露确实是一个具有挑战性的任务。研究特征与目标变量之间的关系是揭示这个问题的关键。下次当你看到模型的性能异常高时,也许更好地学会坐下来观察,因为不是所有事情都需要反应。
感谢阅读,祝建模愉快!
如果你从这篇文章中获得了有用的信息,请考虑在 Medium 上给我一个 关注。简单,每周一篇文章,保持更新,领先一步!
你可以在 LinkedIn 上与我联系: https://www.linkedin.com/in/andreaslukita7/
参考文献:
-
Forcepoint. 什么是数据泄露?数据泄露的定义、解释和探索。
www.forcepoint.com/cyber-edu/data-leakage
-
Analytics Vidhya. 数据泄露及其对机器学习模型性能的影响。
www.analyticsvidhya.com/blog/2021/07/data-leakage-and-its-effect-on-the-performance-of-an-ml-model/
-
JFrog. 小心数据泄露——你的机器学习模型中的潜在陷阱。
jfrog.com/community/data-science/be-careful-from-data-leakage-2/
-
Michael Walker 的《机器学习中的数据清理与探索》:
www.packtpub.com/product/data-cleaning-and-exploration-with-machine-learning/9781803241678
-
Scikit-learn Pipeline.
scikit-learn.org/stable/modules/generated/sklearn.pipeline.Pipeline.html#sklearn.pipeline.Pipeline
-
Titanic 数据集。
www.openml.org/search?type=data&status=active&id=40945&sort=runs
DVC 指南:所有数据科学项目的数据版本控制
使数据版本控制与代码版本控制一样熟悉
·发表于Towards Data Science ·6 分钟阅读·2023 年 2 月 17 日
--
图片由Dmitri Sobolevski提供,来源于Unsplash
作为数据科学家,我们对不同版本的代码、模型和数据进行实验。此外,我们甚至使用像 Git 这样的版本控制系统来管理我们的代码、跟踪版本、前进和后退,并与团队分享我们的代码。
代码的版本控制很重要,因为它有助于在更大规模上重现软件。数据的版本控制之所以重要,是因为它有助于在任何时间点由团队或组织中的任何开发人员使用相似的指标开发机器学习模型。
因此,版本控制你的模型和数据至关重要。但经验丰富的软件工程师会知道,使用 Git 来存储大文件是一个大忌。
Git 不仅在处理大文件时效率低下,而且也不是存储大数据文件的标准化环境。大多数数据存储在 AWS S3 桶、Google Cloud Storage 或任何机构的远程存储服务器上。
那么我们如何对数据进行版本控制呢?这就引出了 DVC。
介绍 DVC
DVC是一个数据版本控制系统,与 Git 密切配合以跟踪我们的数据文件。它甚至有类似于 Git 的语法,所以学习起来相当容易。
让我们在这篇文章中深入了解 DVC 的一些优秀数据版本控制功能。但首先,让我们创建一个新的项目文件夹和虚拟环境,并将其作为 Python 包进行安装:
$ pip install "dvc[all]"
或者如果你使用Pipenv:
$ pipenv shell
$ pipenv install "dvc[all]"
你应该会看到如下输出:
安装 DVC:图片来自作者
现在,让我们初始化一个 git 仓库。你应该会看到以下输出:
dvc init:作者提供的图像
完美!我们现在可以继续将数据添加到 DVC 中。
将数据添加 + 提交到 DVC
我在项目的数据文件夹中有一个数据文件,如下所示:
文件夹结构:作者提供的图像
要从终端运行大小检查,请使用:
$ ls -lh data
你将看到以下输出,数据文件显示为 5.2 MB。
数据文件检查:作者提供的图像
现在,我们可以将这个数据文件添加到 DVC。运行:
$ dvc add data/train_shakespeare.txt
你将看到以下输出,提示我们运行 git add 命令:
dvc 添加的数据:作者提供的图像
我们现在将运行 git add 命令:
$ git add data/train_shakespeare.txt.dvc data/.gitignore
现在我们已经将新的 .dvc 文件添加到我们的 git 跟踪中,我们可以继续提交它到我们的 git:
$ git commit -m "added data."
为我们的数据设置远程存储
我们可以简单地利用 Google Drive 存储我们的版本化数据集,在本教程中我们将正是这样做。
让我们在 Google Drive 中创建一个新文件夹,并查看其 URL:
https://drive.google.com/drive/u/0/folders/cVtFRMoZKxe5iNMd-K_T50Ie
粗体突出显示的是我们想要复制到终端的文件夹 ID,以便 DVC 可以在新创建的 Drive 文件夹中跟踪我们的数据。
让我们来做:
$ dvc remote add -d storage gdrive://cVtFRMoZKxe5iNMd-K_T50Ie
现在是将我们的更改提交到 git 的时候了:
$ git commit .dvc/config -m "Configured remote storage."
完美!现在我们可以将数据推送到远程存储。
$ dvc push
它会要求输入认证代码,或直接带你到浏览器进行认证,只需按照指示操作即可。
Google Drive 中的远程数据:作者提供的图像
拉取远程数据
如果你或你的同事想要访问远程存储的数据,可以使用 pull 命令完成。
但首先,让我们删除本地存储的数据及其缓存,以便我们可以从远程拉取:
$ rm -f data/train_shakespeare.txt
$ rm -rf .dvc/cache
现在,拉取:
$ dvc pull
你会看到拉取文件时的以下输出:
从远程拉取文件:作者提供的图像
如你所见,一旦 dvc 追踪你的数据文件,从远程存储拉取它就变得轻而易举。
跟踪数据的不同版本
想象一下,如果我们想跟踪同一个数据文件的新版本,我们可以轻松地将其添加到 dvc,然后再次添加到 git:
$ dvc add data/train_shakespeare.txt
$ git add data/train_shakespeare.txt.dvc
现在,你会看到一个新的 .dvc 文件版本准备提交到我们的 git:
提交新的 .dvc 文件更改:作者提供的图像
提交文件。
现在,我们可以将最新的数据集推送到远程存储:
$ dvc push
查看我们的 Google Drive,我们可以看到我们有两个版本的数据存储:
Google Drive 数据版本:作者提供的图像
返回到不同的数据集版本
使用 DVC,回到数据集的旧版本变得容易。
如果我们查看到目前为止项目的 git log,我们会看到我们已经将两个 .dvc 文件版本提交到 git:
因此,我们必须返回到之前的.dvc文件版本,因为这是 git 正在跟踪的版本。
首先,简单地执行 Git checkout 到较旧的提交,如下所示:
$ git checkout HEAT¹ data/train_shakespeare.txt.dvc
第二步,进行 dvc 的 checkout:
$ dvc checkout
你会看到以下输出。我们现在已经将数据文件恢复到之前的版本了!
恢复数据到之前的版本:来自作者的图像
此外,如果你想保留这些数据集更改,只需再次提交到 git:
$ git commit data/train_shakespeare.txt.dvc -m "reverted data changes."
完美!到目前为止,你已经学习了 DVC 的大部分基本数据版本控制功能。干得好!
最后的话…
DVC 在数据科学项目的数据版本控制中提供了巨大的帮助,希望你在阅读完这篇文章后,能对如何开始使用 DVC 有一些有用的了解。
在一些示例项目中进行实践,并探索 DVC 文档,将是提升你使用这个惊人工具的技能的最佳方法。
如果你喜欢这篇文章,每周我都会发布一篇故事,在其中分享来自数据科学和编程领域的小知识。关注我,绝不错过! 😄
你可能会喜欢的另外几篇文章:
## 可重用的 Python 日志模板,适用于所有数据科学应用 ## 使用 Docker Compose 和 GitHub Actions 的简单 Python CI/CD 管道
## 使用 Docker Compose 和 GitHub Actions 的简单 Python CI/CD 管道 ## 可重用的 Python 日志模板,适用于所有数据科学应用
在 VPS 上持续部署一个实际项目
## 使用 Docker Compose 和 GitHub Actions 的简单 Python CI/CD 管道
动态批量模型:一种混合整数编程方法
经典库存管理优化问题,使用 Python 和 Pyomo 解决
·
关注 发表在 Towards Data Science · 7 分钟阅读 · 2023 年 2 月 1 日
--
图片由 CHUTTERSNAP 提供,来源于 Unsplash
批量定制问题是具有生产批次之间设置的生产计划问题。由于这些设置,通常在每个周期内生产给定产品的成本过高(Suwondo & Yuliando, 2012)。相比之下,较少的设置与更高的持有库存成本相关。因此,为了获得最佳成本,需要平衡这些操作方面。
在本文中,Wagner & Whitin(1958)提出的问题将使用混合整数规划方法进行实现。为此,将使用 Python 库pyomo(Bynum 等,2021)和CBC 求解器。个人而言,我发现这个问题对于介绍离散规划中的库存管理概念非常有用,这在更复杂的问题中随处可见。请注意,在他们的原始工作中,Wagner & Whitin(1958)开发了一种求解该问题的精确算法,这与这里采用的方法有所不同。
你可以在这里找到包含问题完整实现的笔记本。
如果你对数值优化和/或混合整数规划不熟悉,你可能需要先查看这个关于该主题的介绍。
学习如何使用 scipy 和 pyomo 在 Python 中解决优化问题
towardsdatascience.com
问题陈述
假设在给定的时间范围T内,你可以预测每个时间点t的给定产品的需求d。此外,假设你可以预测每个时间点的设置成本s和库存持有成本h。如果在某个时间点t生产了任何数量的产品,则应发生相应的设置成本s。此外,在每个周期结束时,应产生持有库存成本,对应于h乘以t的期末库存。
如果问题是非限制性的,如 Wagner & Whitin(1958)所考虑的,可以认为每个需求都应得到满足。此外,假设单位生产成本与时间无关,尽管问题结构的微小变化可能会轻易引入这些成本。你的目标是确定每一时刻生产多少单位的产品,以便最小化设置和持有库存成本。
我将在下一节中详细描述每个建模组件,但可以通过以下方程总结所考虑的完整优化问题。
动态批量模型作为混合整数规划问题。 (图片由作者提供)。
建模
首先,让我们导入所使用的 Python 库:
import numpy as np
import pandas as pd
import pyomo.environ as pyo
import matplotlib.pyplot as plt
现在,让我们按照原始文章(Wagner & Whitin, 1958)中的示例来实例化数据集。
months = np.arange(12, dtype=int) + 1
setup_cost = np.array([85, 102, 102, 101, 98, 114, 105, 86, 119, 110, 98, 114])
demand = np.array([69, 29, 36, 61, 61, 26, 34, 67, 45, 67, 79, 56])
inventory = np.ones(12)
dataset = pd.DataFrame(
{"setup_cost": setup_cost, "inventory_cost": inventory, "demand": demand},
index=months,
)
由于我们在这个示例中使用了 Pyomo,我们必须首先实例化 pyomo 模型。在这个示例中,我选择使用 ConcreteModel 方法,因为我发现它对于简单问题更直接。或者,也可以使用 AbstractModel。这两种方法在这里有更详细的描述。
model = pyo.ConcreteModel()
在这个问题中,唯一被考虑的集合是计划时间范围 T。让我们将其实例化为一个 pyomo 对象。
model.T = pyo.Set(initialize=list(dataset.index))
接下来,我们将实例化从问题数据集中导入的模型参数。它们如下:
-
d:每个时间 t 的需求
-
s:每个时间 t 的设定成本
-
h:每个时间 t 的持有库存成本
model.d = pyo.Param(model.T, initialize=dataset.demand)
model.s = pyo.Param(model.T, initialize=dataset.setup_cost)
model.h = pyo.Param(model.T, initialize=dataset.inventory_cost)
以及决策变量:
-
x:每个时间 t 生产的产品数量
-
y:二进制变量,标记每个时间 t 的设定成本
-
I:每个时间 t 的最终库存
model.x = pyo.Var(model.T, within=pyo.NonNegativeReals)
model.y = pyo.Var(model.T, within=pyo.Binary)
model.I = pyo.Var(model.T, within=pyo.NonNegativeReals)
库存平衡约束表示在某一时期 t 的库存等于前一时期末的库存加上生产量减去该时期的需求。我们在这里假设初始库存为零。
库存平衡约束。 (图片由作者提供)。
def inventory_rule(model, t):
if t == model.T.first():
return model.I[t] == model.x[t] - model.d[t]
else:
return model.I[t] == model.I[model.T.prev(t)] + model.x[t] - model.d[t]
model.inventory_rule = pyo.Constraint(model.T, rule=inventory_rule)
现在让我们制定 big M 约束以识别具有设定成本的时期。我们的目的是,如果在时间 t 生产了任何数量的产品,则 y 应为 1,否则为 0。
大 M 约束。 (图片由作者提供)。
通常,你可能会想选择一个足够大的 M 值,以避免从整数决策空间中去除可行解。这样可以在 分支定界算法 中产生更好的 best-bound 值,而不是选择任意大的值。
在这个问题中,只有在持有库存成本从时间 t-n 到 t 小于或等于 t 时的设定成本时,才有意义在时间 t-n 生产一定需求量 d。因此,我们可以通过以下几行计算某一时刻 t 的“最大智能生产”:
def get_max_antecip(t, dataset=dataset):
"""
Returns the first instant in which it might be
intelligent to produce a certain demand
"""
total_inv = 0
d = dataset.demand[t]
s = dataset.setup_cost[t]
out = t
for i in range(1, t - dataset.index[0] + 1):
h = dataset.inventory_cost[t - i]
if total_inv + h * d <= s:
total_inv = total_inv + h * d
out = t - i
else:
break
return out
def get_max_prod(t, dataset=dataset):
df = dataset.query(f"max_antecip <= {t} & index >= {t}")
return df.demand.sum()
dataset["max_antecip"] = [get_max_antecip(t, dataset=dataset) for t in dataset.index]
dataset["max_prod"] = [get_max_prod(t, dataset=dataset) for t in dataset.index]
现在我们可以为每个时间段定义一个合适的 M 值,并实例化 big M 约束。
model.M = pyo.Param(model.T, initialize=dataset.max_prod)
def active_prod(model, t):
return model.x[t] <= model.M[t] * model.y[t]
model.active_prod = pyo.Constraint(model.T, rule=active_prod)
最后,目标函数将计划时间范围内所有持有库存和设定成本相加。
目标函数。 (图片由作者提供)。
def total_cost(model):
holding = sum(model.h[t] * model.I[t] for t in model.T)
setup = sum(model.s[t] * model.y[t] for t in model.T)
return setup + holding
model.obj = pyo.Objective(rule=total_cost, sense=pyo.minimize)
为了解决模型,我使用了开源求解器 CBC。你可以从 AMPL 或 这个链接 下载 CBC 二进制文件。你还可以在 这里 找到安装教程。由于 CBC 可执行文件已包含在我的系统 PATH 变量中,我可以在不指定可执行文件路径的情况下实例化求解器。如果你的系统没有包含,可通过关键字参数“executable”解析到你的可执行文件路径。
# executable=YOUR_PATH
solver = pyo.SolverFactory("cbc")
另外,也可以使用 GLPK 来解决这个问题(或任何其他与pyomo兼容的求解器)。最新的 GLPK 版本可以在 这里 找到,Windows 可执行文件可以在 这里 找到。
solver.solve(model, tee=True)
完成了!结果已存储在我们的模型中,并将在下一节中详细分析。
结果
首先,让我们打印简单场景的成本,其中每个需求都应该在其对应的瞬时生产。
# Obtain the maximum cost for comparison
max_cost = dataset.setup_cost.sum()
print(f"Maximum cost: {max_cost:.1f}")
这应该返回 1234.0。
接下来,让我们从目标函数中获取新的成本并比较结果:
opt_value = model.obj()
print(f"Best cost {opt_value}")
print(f"% savings {100 * (1 - opt_value / max_cost) :.2f}")
哇!新的成本是 864.0,节省了近 30%!
让我们在数据集中添加新列,并绘制生产与需求的关系,以便更好地可视化结果:
# Include production as a column
dataset["production"] = [model.x[t].value for t in dataset.index]
# Plot figure
fig, ax = plt.subplots(figsize=[6, 3], dpi=100)
x = dataset.index
width = 0.35
ax.bar(x - width/2, dataset.production, width, color="darkgreen", label="production")
ax.bar(x + width/2, dataset.demand, width, color="navy", label="demand")
ax.set_xticks(x)
ax.set_ylabel("Qtd")
ax.set_xlabel("t")
ax.legend()
fig.tight_layout()
plt.show()
计划期结果。(图像由作者提供)。
可以注意到,除了瞬时 10(其只有一个需求)之外,其他需求被组合在同一生产瞬时,以显著减少整体设置成本。
结论
在这篇文章中,动态批量模型(Wagner & Whitin, 1958)被形式化并解决为一个混合整数规划问题。与简单的解决方案相比,优化结果可以将总体成本减少 30%。在这种情况下,库存持有和设置成本得到了合理平衡。完整的解决方案可以在这个 git 仓库 中找到。
进一步阅读
对整数规划有更多兴趣的人可以参考 Wolsey (2020);有关运筹学的内容,可以参考 Winston & Goldberg (2004)。
分支限界算法是解决整数和混合整数问题中最常用的算法。有兴趣了解其机制的读者可以参考我之前的 Medium 文章。
参考文献
Bynum, M. L. 等,2021。Pyomo-optimization modeling in python. Springer。
Suwondo, E., & Yuliando, H. (2012). 动态批量问题:关于模型和高效算法的综述。农业工业期刊, 1(1), 36。
Wagner, H. M., & Whitin, T. M. (1958). 动态版本的经济批量模型。管理科学, 5(1), 89–96。
Winston, W. L. & Goldberg, J. B., 2004. 运筹学:应用与算法. 第 4 版. 加州贝尔蒙特:汤姆森·布鲁克斯/科尔贝尔蒙特。
Wolsey, L. A., 2020. 整数规划. 第 2 版. 约翰·威利与儿子公司。
使用大型语言模型的最简单方法?
LangChain 概述
·
关注 发表在 Towards Data Science ·7 分钟阅读·2023 年 3 月 26 日
--
LangChain 是与大型语言模型交互和构建应用程序的最简单方法吗?它是一个开源工具,并且最近添加了 ChatGPT 插件。它提供了许多我认为有用的功能:
✨ 与包括 OpenAI、Cohere、Huggingface 在内的各种 LLM 提供商集成。
✨ 使用你自己的文档创建一个问答或文本摘要机器人
✨ 提供 OpenAI ChatGPT Retriever 插件
✨ 使用 LangChain Memory 处理聊天记录
✨ 将各种 LLM 结合在一起,并使用一系列工具,如 Google 搜索、Python REPL 等。
✨ 还有更多功能
更令人惊讶的是,LangChain 是开源的,这是一项社区努力。在这篇博客文章中,我将介绍我认为 LangChain 中有用的 6 个功能。
你可以在我的 Github repo here 找到这篇博客文章的代码。你需要安装所需的包,导入包,并设置 API 密钥(请参见 my notebook 中的第 1–3 个单元格)然后再运行下面的代码。
1. LangChain 与许多 LLM 提供商集成
使用相同的接口,你可以访问来自多个 LLM 提供商的许多 LLM 模型,包括 OpenAI、Cohere、AI21、Huggingface Hub、Azure OpenAI、Manifest、Goose AI、Writer、Banana、Modal、StochasticAI、Cerebrium、Petals、Forefront AI、PromptLayer OpenAI、Anthropic、DeepInfra 和自托管模型。
这是访问 OpenAI ChatGPT 模型和 GPT3 模型、Cohere 的 command-xlarge 模型,以及托管在 Hugging Face Hub 上的 Google 的 Flan T5 模型的示例。
chatgpt = ChatOpenAI(model_name='gpt-3.5-turbo')
gpt3 = OpenAI(model_name='text-davinci-003')
cohere = Cohere(model='command-xlarge')
flan = HuggingFaceHub(repo_id="google/flan-t5-xl")
然后我们可以给每个模型提供一个提示,看看它们返回什么。需要注意的是,对于聊天模型,我们可以指定消息是人类消息、AI 消息还是系统消息。这就是为什么对于 ChatGPT 模型,我将我的消息指定为人类消息。
2. 外部文档的问答
你对使用外部文档构建问答机器人感兴趣吗?你的文档保存在 PDF、txt 文件,甚至 Notion 中吗?那如果我还想要基于 YouTube 视频进行问答怎么办?
没问题,LangChain 能满足你的需求。LangChain 提供了许多文档加载器:文件加载器、目录加载器、Notion、ReadTheDocs、HTML、PDF、PowerPoint、电子邮件、GoogleDrive、Obsidian、Roam、EverNote、YouTube、Hacker News、GitBook、S3 文件、S3 目录、GCS 文件、GCS 目录、Web 基础、IMSDb、AZLyrics、College Confidential、Gutenberg、Airbyte Json、CoNLL-U、iFixit、Notebook、Copypaste、CSV、Facebook Chat、图片、Markdown、SRT、Telegram、URL、Word 文档、Blackboard。你有其他没有列出的来源吗?因为 LangChain 是开源的,你可以贡献并添加到 LangChain 中!
这里是一个示例,我们使用 TextLoader 加载本地 txt 文件,创建 VectoreStore 索引,并查询你的索引。直观地说,你的文档将被拆分并嵌入到向量中,并存储在向量数据库中。查询将找到在向量数据库中最接近问题向量的向量。
令人兴奋的消息是 LangChain 最近集成了ChatGPT Retrieval Plugin,因此人们可以使用这个检索器代替索引。索引和检索器有什么区别?根据LangChain, “索引是一种支持高效搜索的数据结构,而检索器是使用索引来查找并返回与用户查询相关的文档的组件。索引是检索器执行其功能所依赖的关键组件。”
3. 保留/总结聊天历史
当您与 ChatGPT 聊天时,它不会保留您的聊天历史记录。您需要将聊天内容复制并粘贴到新的提示中,以便它能够了解历史记录。LangChain 通过提供处理聊天历史记录的几种不同选项来解决这个问题——保留所有对话、保留最新的 k 次对话、总结对话以及上述选项的组合。
这是一个示例,我们使用ConversationBufferWindowMemory
来保持最新的 1 轮对话,使用ConversationSummaryMemory
来总结之前的对话,并使用CombinedMemory
来结合这两种方法:
from langchain.llms import OpenAI
from langchain.prompts import PromptTemplate
from langchain.chains import ConversationChain
from langchain.memory import ConversationBufferWindowMemory, CombinedMemory, ConversationSummaryMemory
conv_memory = ConversationBufferWindowMemory(
memory_key="chat_history_lines",
input_key="input",
k=1
)
summary_memory = ConversationSummaryMemory(llm=OpenAI(), input_key="input")
# Combined
memory = CombinedMemory(memories=[conv_memory, summary_memory])
_DEFAULT_TEMPLATE = """The following is a friendly conversation between a human and an AI. The AI is talkative and provides lots of specific details from its context. If the AI does not know the answer to a question, it truthfully says it does not know.
Summary of conversation:
{history}
Current conversation:
{chat_history_lines}
Human: {input}
AI:"""
PROMPT = PromptTemplate(
input_variables=["history", "input", "chat_history_lines"], template=_DEFAULT_TEMPLATE
)
llm = OpenAI(temperature=0)
conversation = ConversationChain(
llm=llm,
verbose=True,
memory=memory,
prompt=PROMPT
)
这里是结果,您可以看到对话摘要以及最新对话传递到提示中的内容。
4. 链接
有时您可能想要链式连接不同的 LLM。例如,
-
您可能希望第一个 LLM 的输出作为第二个 LLM 的输入。
-
或者,您可能希望总结第一和第二个 LLM 的输出,并将摘要作为输入传递给第三个 LLM。
-
或者,您可能希望将您的语言模型与另一个实用工具链进行链式操作,以进行数学运算或 Python 操作。
这是最基本的SimpleSequentialChain
方法的一个示例。第一个链要求 ChatGPT 为一家制造彩色袜子的公司提供一个好的产品名称。ChatGPT 返回了“Rainbow Sox Co.”。第二个链使用这个名称作为输入,并要求 ChatGPT 为公司“Rainbow Sox Co.”编写一句广告语。ChatGPT 写道:“让 Rainbow Sox Co.为您的步伐增添一抹色彩!”
from langchain.chat_models import ChatOpenAI
from langchain import LLMChain
from langchain.prompts.chat import (
ChatPromptTemplate,
HumanMessagePromptTemplate,
)
human_message_prompt = HumanMessagePromptTemplate(
prompt=PromptTemplate(
template="What is a good name for a company that makes {product}?",
input_variables=["product"],
)
)
chat_prompt_template = ChatPromptTemplate.from_messages([human_message_prompt])
chat = ChatOpenAI(temperature=0.9)
chain = LLMChain(llm=chat, prompt=chat_prompt_template)
second_prompt = PromptTemplate(
input_variables=["company_name"],
template="Write a catchphrase for the following company: {company_name}",
)
chain_two = LLMChain(llm=llm, prompt=second_prompt)
from langchain.chains import SimpleSequentialChain
overall_chain = SimpleSequentialChain(chains=[chain, chain_two], verbose=True)
# Run the chain specifying only the input variable for the first chain.
catchphrase = overall_chain.run("colorful socks")
print(catchphrase)
5. 代理
代理可以访问语言模型和一套工具,例如 Google 搜索、Python REPL、数学计算器等。代理使用 LLM 和各种框架来决定为哪些任务使用哪些工具。这与 ChatGPT 插件非常相似。以下是一个示例:
在这个示例中,我们加载了两个工具:serpapi 用于 Google 搜索,llm-math 用于数学计算。我们询问了“莱昂纳多·迪卡普里奥的女朋友是谁?她目前的年龄提高到 0.43 的幂是多少?”语言模型首先理解了问题,并决定执行搜索操作以触发搜索 API 调用。然后它从搜索代理那里获取了结果,决定使用 llm-math 进行计算,最后得到了最终结果。
在另一个例子中,我们可以使用 Python REPL 执行 Python 代码进行计算,而不是使用 llm-math 作为计算器:
6. ChatGPT 插件
ChatGPT 插件刚刚在本周推出,而 LangChain 已经添加了 ChatGPT 插件功能。这里是一个例子,我们使用 AIPluginTool
从购物网站加载插件。这个插件与 OpenAI 使用的完全相同。当我们问“在 Klarna 上有哪些 T 恤?”时,LLM 决定使用 Klarna 插件并得到了一个 OpenAI 规范系列的响应,然后决定发起请求以获取数据,最终返回了很好的答案。
from langchain.chat_models import ChatOpenAI
from langchain.agents import load_tools, initialize_agent
from langchain.tools import AIPluginTool
tool = AIPluginTool.from_plugin_url("https://www.klarna.com/.well-known/ai-plugin.json")
llm = ChatOpenAI(temperature=0)
tools = load_tools(["requests"] )
tools += [tool]
agent_chain = initialize_agent(tools, llm, agent="zero-shot-react-description", verbose=True)
agent_chain.run("what t shirts are available in klarna?")
结论
总体而言,我对 LangChain 的能力和社区快速添加新功能的速度感到非常印象深刻。还有许多其他功能我在这篇博客文章中没有提到,包括数据增强、少量学习、评估、模型比较等。我希望这篇博客文章能帮助你更好地理解各种 LangChain 功能。祝你探索愉快!
. . .
由 Sophia Yang 于 2023 年 3 月 26 日发布
Sophia Yang 是 Anaconda 的高级数据科学家。通过 LinkedIn、Twitter 和 YouTube 与我联系,并加入 DS/ML 读书俱乐部 ❤️
使用 SQLAlchemy 进行 UPSERT 的最简单方法
原文:
towardsdatascience.com/the-easiest-way-to-upsert-with-sqlalchemy-9dae87a75c35
一个命令同时INSERT
新数据和UPDATE
数据库中现有的记录
·发布于 Towards Data Science ·5 min read·2023 年 3 月 20 日
--
在这篇短文中,我们将深入探讨如何在 SQLAlchemy 中进行UPSERT
:我们将INSERT
新数据到数据库中,并用新提供的值UPDATE
已经存在的记录。我们将从一个用例开始,准备数据库,然后进行 UPSERT!非常简单!让我们开始编码吧!
一行行地进行 UPSERT(图片来源 Tom Fisk 在 Pexels)
什么时候我需要进行 UPSERT?
Upserting 是将新记录INSERT
到数据库表中,同时UPDATE
数据集中已经存在的记录。数据库通过比较表的主键列来“知道哪些记录已经存在”。
UPDATE
现有记录很重要,以便我们的外键关系保持完整。在下一部分中,我们将通过一个示例清楚地解释我们在实践中的目标。
在 Python 中创建超快速数据库连接的 4 个级别
towardsdatascience.com
设置示例
想象一下我们经营一个吉他店的网店。当然,我们必须跟踪哪些吉他有库存、库存数量以及价格。这就是inventory
表的作用:
**sku name price stock** LPTD19HSNH1 Gibson Les Paul 2000 6
1-014-9763-300 Fender Jazzmaster 1799 3
IRONMK1KH80BK BC Rich Ironbird 1099 4
每一行都基于sku
是唯一的:这是表的primary key
。主键在插入时非常重要。
我们每天接收到的数据集可能如下所示:
**sku name price stock** LPTD19HSNH1 Gibson Les Paul 2000 4
1-014-9763-300 Fender Jazzmaster 1700 3
DSV00ANCH1 Gibson Flying V 2499 1
我们的目标是将新数据UPSERT
到inventory
表中:
-
INSERT
新记录(如 Gibson Flying V) -
UPDATE
现有记录(例如 Fender Jazzmaster 的新价格)
通过同时执行多个操作来加速你的程序
towardsdatascience.com
使用 SQLAlchemy 进行 UPSERT
我们将使用 SQLAlchemy 进行 3 个步骤的 UPSERT。
在这篇文章中,我假设你对 SQLAlchemy 已经有所了解,所以我不会详细讲解,因为这超出了我们当前的范围,但我会提供更多深入解释的文章链接。还可以查看 这个链接 以获取有关 SQLAlchemy 的更多文章。
1. 设置我们的数据库和连接
首先,我们声明一个名为‘Guitar’的模型。这对应于我们库存表中的一行。注意,它有一个名为sku
的列,作为主键。
# SQLAlchemy needs a declarative_base when we use declarative models like Guitar
Base = declarative_base()
# Guitar will be one record in our inventory table
class Guitar(Base):
__tablename__ = 'inventory'
sku: typing.Optional[str] = sa.Column(type_=sa.String, primary_key=True)
name: str = sa.Column(type_=sa.String)
stock:int = sa.Column(type_=sa.Integer)
price: int = sa.Column(type_=sa.Integer)
def __repr__(self):
return f"<{self.name} (price=${self.price} - stock=#{self.stock}>"
# Create a database engine (sqlite in this case)
dbEngine = sa.create_engine(url="sqlite:///inventory_db.db", echo=False)
# Create all tables in the database engine
Base.metadata.create_all(bind=dbEngine)
接下来,我们为我们的 sqlite 数据库创建一个数据库引擎(查看 这篇文章 以获取更多信息),并使用它来创建inventory
表。
## 永远不需要再写 SQL:SQLAlchemy 的 ORM 为绝对初学者
使用这个 ORM,你可以创建表格、插入、读取、删除和更新数据,而无需编写一行 SQL
towardsdatascience.com
2. 将数据插入到inventory
表中
接下来,我们将创建一些Guitar
模型的实例,并使用 SQLAlchemy 的Session
对象将它们插入数据库:
with Session(dbEngine) as session:
session.add_all([
Guitar(sku="LPTD19HSNH1", name="Gibson Les Paul", price=2000, stock=6),
Guitar(sku="1-014-9763-300", name="Fender Jazzmaster", price=1799, stock=3),
Guitar(sku="IRONMK1KH80BK", name="BC Rich Ironbird", price=1099, stock=4),
])
session.commit()
当我们 SELECT * FROM inventory
时,我们可以看到inventory
表中的内容:
**sku name price stock** LPTD19HSNH1 Gibson Les Paul 2000 6
1-014-9763-300 Fender Jazzmaster 1799 3
IRONMK1KH80BK BC Rich Ironbird 1099 4
接下来,我们将接收一个包含新每日库存的数据集。我们需要插入新的Guitar
记录,并更新我们当前inventory
中的记录。
为初学者创建一个 Docker 容器中的 Postgres 数据库
towardsdatascience.com
3. 将数据 UPSERT 到库存表中
最后是 UPSERT 的时候了!开始吧:
# Convert the new stock to a list of Guitar objects
new_stock:[Guitar] = [
Guitar(sku="LPTD19HSNH1", name="Gibson Les Paul", price=2000, stock=4), # sold 2 les pauls
Guitar(sku="1-014-9763-300", name="Fender Jazzmaster", price=1700, stock=3), # lowered price on jazzmaster
Guitar(sku="DSV00ANCH1", name="Gibson Flying V", price=2499, stock=1), # new guitar in stock!
]
# Use the session and UPSERT using the "merge" method
with Session(dbEngine) as session:
for guitar in new_stock:
session.merge(guitar)
session.commit()
正如你所见,我们使用 session.merge
。此方法分析我们的 new_stock 并检查提供的 sku
值是否已存在于 inventory
表中。如果 sku
存在,它会执行更新,否则它会插入新的吉他。
这是结果表:
**sku name price stock** LPTD19HSNH1 Gibson Les Paul 2000 4 << stock decreased by 2
1-014-9763-300 Fender Jazzmaster 1700 3 << price lowered by $99
IRONMK1KH80BK BC Rich Ironbird 1099 4 << no chnages
DSV00ANCH1 Gibson Flying V 2499 1 << new in stock!
一点需要注意的是,merge
方法与 SQL 中的 MERGE 不同,因为它仅执行插入和更新。如果你想要更细粒度的控制,查看 这篇文章,它展示了如何在 SQL 中用一条语句进行删除/更新/插入。
## 使用 OpenCV 进行运动检测 — 初学者的图像分析
如何使用 OpenCV 检测和分析移动物体
towardsdatascience.com
结论
正如我们所见,当使用 SQLAlchemy ORM 时,UPSERT 非常简单。查看我的其他文章 这里,以了解 Python、SQLAlchemy、Docker 和数据库的一些用途!
我希望这篇文章能够如我所期望的那样清晰,但如果不是这种情况,请告诉我我可以做些什么来进一步澄清。同时,查看我关于各种编程相关主题的 其他文章:
编程愉快!
— Mike
附言:喜欢我所做的工作吗? 关注我!
[## 通过我的推荐链接加入 Medium - Mike Huls
阅读 Mike Huls 的每个故事(以及 Medium 上其他成千上万的作家的故事)。你的会员费用直接支持 Mike…
mikehuls.medium.com](https://mikehuls.medium.com/membership?source=post_page-----9dae87a75c35--------------------------------)
提示工程的崭新艺术
·
关注 发表在 Towards Data Science · 发送至 通讯 · 3 分钟阅读 · 2023 年 5 月 18 日
--
现在,即使是最随意的生成式 AI 爱好者也知道,提示的具体性在很大程度上决定了模型输出的质量——无论是现实的图像、相关的段落,还是实际上有效的代码块。
许多行业领袖声称,提示工程——即调整输入以提高模型的任务表现——是一项有着有限有效期的技能,因为 AI 技术发展迅速。这尚待观察;目前以及可预见的未来,设计更好的提示仍然是一项值得数据和机器学习从业者关注的实用技能。
为了帮助您发展提示技能,我们选择了一些关于这一主题的最佳近期文章,涵盖了各种使用案例和经验水平。让我们开始吧!
-
如果您对与大型语言模型一起工作还很陌生,一个很好的起点是Olivia Tanuwidjaja的面向数据分析师的提示工程指南,该指南提供了对常见方法的高层次(但详尽)概述。
-
LLM(大型语言模型)执行未经专门训练的任务的能力依赖于用户的提示创造力和敏捷思维。举个例子:Jye Sawtell-Rickson尝试使用 ChatGPT 进行探索性数据分析。
Jackie Hope拍摄的照片,来自Unsplash。
-
要展示您新获得的 LLM 知识,Leonie Monigatti提议10 个可以添加到您的作品集的项目想法——并详细介绍了您在此过程中需要使用的各种提示类型。
-
对于语言模型内部工作原理的更深入理解,Scott Lundberg和Marco Tulio Ribeiro讨论了语言模型使用的“贪婪标记化方法可能会在您的提示中引入微妙而强大的偏见,导致令人困惑的生成结果。”
如果您还没有完全满足于思想深刻、专业呈现的文章,想要超越提示设计,我们为您提供:
-
从不同(更加不祥)的角度探索 LLM,Parul Pandey审视了有关语言模型对恶意行为者进行毒化攻击的研究作品[/exploring-the-vulnerability-of-language-models-to-poisoning-attacks-d6d03bcc5ecb]。
-
Sachin Date的最新深度探讨是关于随机变量的全面且严肃但又引人入胜的指南,这正是你一直想要的。
-
在机器学习管道中,保持简单说起来容易,但Hennie de Harder关于构建最简可行产品(MVP)的见解将激励你专注于项目中最重要的元素。
-
量子计算是否即将颠覆统计学的基础?Tim Lou, PhD 解读了赢得 2022 年物理学诺贝尔奖的工作的意义。
-
想了解数据导向编程及其与面向对象编程(OOP)的区别,千万不要错过Tam D Tran-The的有用(并且包含代码片段)的解释。
感谢你支持我们的作者!如果你喜欢在 TDS 上阅读的文章,可以考虑成为 Medium 会员——这将解锁我们的整个档案(以及 Medium 上的所有其他帖子)。
直到下一个变量,
TDS 编辑部
蛋白质设计的机器学习时代,概括为四种关键方法
感谢这些基于人工智能的方法和工具,蛋白质生物技术迎来了如此激动人心的时代
LucianoSphere (Luciano Abriata, PhD)
·发布于 Towards Data Science ·阅读时间 9 分钟·2023 年 5 月 5 日
--
图由作者使用 Dall-E-2 和自定义编辑创建。
蛋白质设计和工程是分子生物学中的重要目标,具有广泛的应用,包括医学、生物技术和材料科学等领域。科学家们已经探索了几十种设计新型蛋白质和工程现有蛋白质以微调其性质的方法。尽管基于物理的方法在找到折叠成特定蛋白质结构的氨基酸序列方面取得了一些成功,但深度学习方法的最新发展显示出了更高的成功率和多样性。在这篇文章中,我将概述四种值得注意的机器学习(ML)工具,用于蛋白质设计和工程,并探讨它们在推动该领域发展中的重要性。
除了这些工具在化学和生物科学领域立即产生的影响之外,它们引入的方法甚至这些项目本身也为数据科学家、机器学习从业者和人工智能研究人员提供了令人兴奋的机会,让他们可以与化学和生物科学家合作,提出新想法和方法,最终将计算机科学应用于有益的领域。实际上,以下我要讨论的工具展示了应用不同种类的深度学习算法来解决生物技术中的复杂挑战的强大能力。通过利用这些工具,数据科学、机器学习和人工智能领域的专业人士也可以为医学、生物技术和材料科学的进步做出贡献,亲眼见证自己领域的直接影响,即使在其领域之外!
简而言之,我将按发布顺序介绍称为 ProteinMPNN、ESM2-InverseFold、RoseTTaFold Diffusion 和 MASIF-Seed 的工具。重要的是,这些模型都在 Deepmind 的 AlphaFold 模型对结构生物学领域产生巨大影响之后开始出名:
[## 一年多的 AlphaFold 2 免费使用及其在生物学中引发的革命
蛋白质结构的自信建模、预测其与其他生物分子的相互作用,甚至蛋白质…
ProteinMPNN
ProteinMPNN,由贝克实验室开发,是第一个获得实验测试设计蛋白质的机器学习工具。
这个模型基于编码器-解码器神经网络,是第一个显示生成经过实验验证能按预期折叠的蛋白质序列的工具。两篇论文,“基于深度学习的蛋白质序列设计使用 ProteinMPNN”和“幻想对称蛋白质组装”,发表于Science期刊的 2022 年底,展示了该方法(前者)和该工具在各种蛋白质设计问题中的适用性(后者)。
我专门写了一篇关于 ProteinMPNN 的博客文章,尽管这篇文章已经算“旧”(尽管发布不到一年,展示了领域发展的速度!)。所以我不会在这里多讲,你可以查看我之前的文章:
贝克实验室的这款新软件设计的蛋白质在湿实验室中实际有效。你可以用它来…
towardsdatascience.com
ESM-InverseFold
由 Meta 开发的 ESM2-InverseFold 基于 ESMFold 蛋白质语言模型,但其设计目的是从结构生成蛋白质序列,而不是从序列预测结构。
发现 ESMFold 能生成在已知自然序列之外高度多样的蛋白质序列。预印本《语言模型超越自然蛋白质的泛化》描述了其核心功能,并展示了几个成功的设计例子。
要了解更多关于 ESMFold 的信息,请查看我之前的文章:
结构预测的准确性与 AlphaFold 相似,但速度快达 60 倍,并且开发了新的 AI 方法。
[towardsdatascience.com
这是基于此的蛋白质设计工具“ESM-InverseFold”的预印本:
从进化过程中序列中学习蛋白质设计模式可能对生成性蛋白质有潜力。
ESM-InverseFold 是一个蛋白质设计工具,利用机器学习生成前所未见的新蛋白质。该工具基于语言模型,这些模型通过掩码语言建模在进化过程中对数百万种不同的自然蛋白质进行了训练。这些模型生成的基序将序列与结构设计关联,并能在新的序列和结构背景中应用它们。ESM-InverseFold 提供了两种生成性蛋白质设计任务:固定骨架设计和自由生成。固定骨架设计通过从语言模型指定的条件分布中使用马尔科夫链蒙特卡洛法和模拟退火来生成蛋白质序列。自由生成完全移除结构上的约束,通过从语言模型指定的序列和结构的联合分布中进行采样来生成新蛋白质。ESM-InverseFold 显示出高实验成功率,在 67%的评估蛋白质中通过尺寸排除色谱法产生了可溶解且单体化的物种。正如作者所示,该工具使用的语言模型能够访问超越自然蛋白质的设计空间,基于蛋白质设计的深层模式生成新颖的解决方案,包括自然蛋白质中的结构基序。
RoseTTAFold Diffusion
基于扩散模型的 RoseTTAFold Diffusion 是贝克实验室最新的工具,也已在 bioRxiv 上预印。
[## 广泛适用且准确的蛋白质设计,通过整合结构预测网络和…
最近在使用深度学习方法设计新蛋白质方面取得了相当大的进展[1][1]-[9][2]。尽管…
来自 Baker 实验室博客,目前 Rosetta 套件中表现最好的蛋白质设计方法是:
由 Baker 实验室科学家 Joseph Watson、David Juergens、Nate Bennett、Brian Trippe 和 Jason Yim 领导的团队创建了…
RoseTTaFold Diffusion 是一种基于去噪扩散概率模型的生成模型,利用深度学习从简单的分子规格生成多样、复杂且功能性的蛋白质。它通过在蛋白质结构去噪任务上微调 RoseTTaFold 结构预测网络,获得蛋白质主链的生成模型。RoseTTaFold Diffusion 通过模拟在训练过程中从蛋白质数据银行中采样的结构的噪声过程生成蛋白质结构。该方法通过将先前步骤中的噪声坐标转化为预测结构来生成新的蛋白质结构,这些预测结构以模型的输入为条件,这些输入可以包括部分序列、折叠信息或固定功能基序坐标。该方法使用两种不同策略进行训练:1)类似于“经典”扩散模型的方式,在每个时间步的预测独立于前一个时间步的预测;2)具有自我条件化,模型可以在时间步之间条件化先前的预测。RoseTTaFold Diffusion 可以在没有额外输入的情况下生成蛋白质结构,也可以通过对各种输入进行条件化来生成蛋白质结构,并且能够生成与任何已知蛋白质结构具有较少总体结构相似性的多样蛋白质结构。该方法在蛋白质结构生成方面优于其他深度学习方法,并且在广泛的设计挑战中表现出最先进的性能,包括蛋白质单体设计、蛋白质结合体设计、对称寡聚体设计、酶活性位点支架设计以及治疗性和金属结合蛋白质设计的对称基序支架设计。
MaSIF-seed
MaSIF-seed,由Michael Bronstein实验室和我的机构(EPFL 扩展学院)的 Correia 实验室共同合作,并于本月在Nature上发表,专注于通过学习的蛋白质表面指纹设计蛋白质相互作用:
## De novo design of protein interactions with learned surface fingerprints - Nature
蛋白质之间的物理相互作用对于大多数生命过程至关重要。然而,...
## De novo design of protein interactions with learned surface fingerprints - Nature
该工具在设计蛋白质单体和聚合物方面表现出色,包括目标结合蛋白质和自然界中未见的结构。它基于自身团队的前期工作 Masif,一种从表面特征预测相互作用的机器学习工具。
与其他方法相比,Masif-seed 采用以表面为中心的方法,专注于蛋白质的表面特性及表面斑点之间的相互作用。它的神经网络输出向量指纹描述符,这些描述符在相互作用的蛋白质对的斑点之间是互补的,而在非相互作用的对之间则不同。匹配的表面斑点与目标位点对齐,并用第二个神经网络进行评分,该网络输出界面后对齐分数,以进一步提高表面描述符的区分性能。与其他工具相比,MaSIF-seed 在基于丰富的表面特征区分真实结合物和诱饵方面表现出色。此外,它比其他方法假设上更快且更准确。
介绍该方法的论文描述了多个使用该工具设计全新蛋白质结合物以接触具有挑战性和与疾病相关的蛋白质靶点的例子。使用 MaSIF-seed 的完整蛋白质设计流程包括几个步骤,从识别具有高结合倾向的蛋白质靶点位点开始,然后从源自片段的表面指纹数据库中搜索子集,以寻找可能靶向所选位点的结合种子,然后使用专门的 Rosetta 协议将其移植到与种子的结合模式兼容的蛋白质支架上。最后,优化结合物界面,并在实际应用中,通过突变库实验筛选设计,以微调最终序列。
设计蛋白质序列以折叠并按科学家的需求工作
在所有这四种工具中,模型的输入是一个骨架结构,可能有某些氨基酸身份被限制,模型会在此基础上制作预期按设计折叠的蛋白质序列。虽然这些模型可以生成相互作用的蛋白质序列,但它们无法在设计过程中原生考虑非蛋白质分子。这一限制阻碍了它们在涉及与非蛋白质分子结合的设计中的应用,除非用户根据所需功能手动修正某些残基。尽管这种策略有些低效,因为需要了解感兴趣的系统,但它已在 2023 年初由贝克实验室在酶的设计中取得了成功:
从头设计酶的目标是引入预测能催化反应的活性位点和底物结合口袋…
就像那个例子一样,这些工具的发展为设计新型蛋白质和工程改造现有蛋白质开辟了激动人心的可能性。这些工具在药物开发、材料科学和生物技术领域特别有用,可以将蛋白质的性质精细调节以满足特定需求。生成经过实验验证的按预期折叠的蛋白质序列的能力,对新治疗方法和疗法的开发具有巨大意义,特别是对于复杂疾病。例如,请参见这种特殊类型的类似疫苗的制剂,它由计算机设计的蛋白质表位混合而成——目前使用的是更传统的物理工具。
此外,这些工具有可能显著减少蛋白质设计和工程所需的时间和资源,使这一研究领域更加可及。它们也更容易部署和运行,这进一步帮助了其使用的民主化。实际上,看看你可以多么轻松地将常规 ESMFold 适配为分析现实蛋白质设计,例如来自 HuggingFace 上运行的 ProteinMPNN,仅在你的网页浏览器中:
## 通过共识方法设计稳定蛋白质的网页应用,使用 JavaScript、ESMFold 创建…
融合现代技术和工具进行高效工作,创建一个实现最简单但如今最…
javascript.plainenglish.io](https://javascript.plainenglish.io/a-web-app-to-design-stable-proteins-via-the-consensus-method-created-with-javascript-esmfold-and-d319d2441ae7?source=post_page-----d6f1dac5de96--------------------------------)
总结来说,我们可以毫不犹豫地声明,在经历了 AlphaFold 的蛋白质结构预测炒作之后,我们现在正处于蛋白质设计的炒作浪潮中,每月都有新方法出现,而我在这里介绍了我认为目前最相关的四种方法——主要因为它们都经过了实验验证。
这些新的蛋白质设计模型展示了令人印象深刻的结果,并且无疑将在不久的将来成为蛋白质生物技术实验室和公司的重要组成部分。尽管仍存在限制,这些工具的潜在应用巨大,预计在未来几年将对医学、生物技术和材料科学产生重大影响。
相关文章
了解计算机建模、模拟和人工智能如何影响蛋白质工程,请查看此内容:
概述了不同复杂度、成功率和应用的计算方法,并指向关键…
在这篇文章中,我探讨了为什么蛋白质设计/工程问题如此困难,即便是针对单一残基:
[## 论文总结:为什么预测稳定性变化如此困难,当一种…
Louis 和 Abriata。分子生物技术 2021 [开放访问这里]
lucianosphere.medium.com](https://lucianosphere.medium.com/why-is-it-so-difficult-to-predict-the-changes-in-stability-that-result-when-a-protein-is-mutated-2df96b2037c5?source=post_page-----d6f1dac5de96--------------------------------)
你可能还会对我关于在科学中平衡机器学习质量和数量的文章感兴趣,其中我特别讨论了与蛋白质设计的 ML 模型相关的内容:
对于适当的验证和良好的数据集的需求,客观和均衡,以及预测在现实中有用的…
[towardsdatascience.com
www.lucianoabriata.com 我撰写并拍摄关于我广泛兴趣领域的一切内容:自然、科学、技术、编程等。 成为 Medium 会员 以访问所有故事(平台的会员链接,您无需付费即可获得我少量收入)并 订阅以通过电子邮件获取我的新故事 。要 咨询关于小项目的事宜, 请查看我的 服务页面。您可以 在这里联系我。
用一行代码构建一个分割模型
原文:
towardsdatascience.com/the-essential-library-to-build-segmentation-models-6e17e81338e
以最快的方式构建和训练图像分割神经网络模型
·发表在Towards Data Science ·阅读时间 6 分钟·2023 年 3 月 6 日
--
MartinThoma,CC0,通过 Wikimedia Commons(已编辑)
神经网络模型在解决分割问题上已被证明非常有效,达到了最先进的准确率。它们在医学图像分析、自动驾驶、机器人技术、卫星图像、视频监控等各种应用中取得了显著的改进。然而,构建这些模型通常需要较长时间,但阅读完本指南后,你将能够用仅仅几行代码来构建一个模型。
目录
-
介绍
-
构建模块
-
构建模型
-
训练模型
介绍
分割是将图像根据某些特征或属性划分为多个片段或区域的任务。一个分割模型以图像为输入,并返回一个分割掩码:
(左)输入图像 | (右)其分割掩码。两张图像均来自PyTorch。
分割神经网络模型由两个部分组成:
-
一个编码器:接受输入图像并提取特征。编码器的例子包括 ResNet、EfficientNet 和 ViT。
-
一个解码器:提取特征并生成分割掩码。解码器的架构各不相同。例如的架构包括 U-Net、FPN 和 DeepLab。
因此,在为特定应用构建分割模型时,你需要选择一个架构和一个编码器。然而,在不测试多个组合的情况下,很难选择最佳组合。这通常需要很长时间,因为更改模型需要编写大量的样板代码。Segmentation Models 库解决了这个问题。它允许你通过指定架构和编码器在一行代码中创建一个模型。然后,你只需修改该行代码即可更改其中的一个。
要从 PyPI 安装最新版本的 Segmentation Models,请使用:
pip install segmentation-models-pytorch
构建模块
该库为大多数分割架构提供了一个类,每个类都可以与任何可用的编码器一起使用。在下一部分中,你将看到要构建模型,你需要实例化所选架构的类,并将所选编码器的字符串作为参数传递。下图显示了库提供的每个架构的类名称:
库提供的所有架构的类别名称。
下图显示了库提供的最常见编码器的名称:
库提供的最常见编码器的名称。
目前有超过 400 种编码器,因此不可能一一展示,但你可以在这里找到完整的列表。
构建模型
一旦从上面的图中选择了架构和编码器,构建模型就非常简单:
参数:
-
encoder_name
是所选编码器的名称(例如:resnet50、efficientnet-b7、mit_b5)。 -
encoder_weights
是预训练数据集。如果encoder_weights
等于"imagenet"
,则编码器权重将使用 ImageNet 预训练权重初始化。所有编码器至少有一种预训练模型,完整列表可以在这里找到。 -
in_channels
是输入图像的通道数(如果是 RGB,则为 3)。即使
in_channels
不是 3,也可以使用 ImageNet 预训练模型:第一层将通过重用预训练的第一个卷积层的权重来初始化(该过程描述这里)。 -
out_classes
是数据集中的类别数量。 -
activation
是输出层的激活函数。可选的值有None
(默认)、sigmoid
和softmax
。注意: 当使用期望 logits 作为输入的损失函数时,激活函数必须为 None。例如,当使用
CrossEntropyLoss
函数时,activation
必须为None
。
训练模型
本节展示了执行训练所需的所有代码。然而,这个库不会改变训练和验证模型的常规流程。为了简化过程,该库提供了许多损失函数的实现,例如Jaccard Loss, Dice Loss, Dice Cross-Entropy Loss, Focal Loss,以及Accuracy, Precision, Recall, F1Score, 和 IOUScore等指标。有关它们及其参数的完整列表,请查阅Losses和Metrics部分的文档。
提议的训练示例是使用Oxford-IIIT Pet Dataset进行二分类分割(将通过代码下载)。以下是数据集中的两个样本:
Oxford-IIIT Pet Dataset 中的一只猫的样本。
Oxford-IIIT Pet Dataset 中的一只狗的样本。
最后,这些是执行此类分割任务的所有步骤:
- 构建模型。
根据你将使用的损失函数设置最后一层的激活函数。
- 定义参数。
请记住,在使用预训练模型时,输入应通过使用训练预训练模型时的数据的均值和标准差来进行归一化。
- 定义训练函数。
这里没有改变,你在不使用库的情况下训练模型时所编写的训练函数。
- 定义验证函数。
真阳性、假阳性、假阴性和真阴性从批次中一起求和,仅在批次结束时计算指标。请注意,logits 必须转换为类别后才能计算指标。调用训练函数开始训练。
- 使用模型。
这些是一些分割示例:
结论
这个库包含了你实验分割所需的一切。构建模型和应用更改非常简单,并且大多数损失函数和指标都已提供。此外,使用这个库不会改变我们习惯的流程。有关更多信息,请参见官方文档。我还在参考文献中包括了一些最常见的编码器和架构。
Oxford-IIIT Pet Dataset在创作共用署名-相同方式共享 4.0 国际许可协议下可供下载用于商业/研究目的。版权归图像的原始所有者所有。
所有图像,除非另有说明,均由作者提供。感谢阅读,希望你觉得这些信息有用。
[1] O. Ronneberger, P. Fischer 和 T. Brox, U-Net: 用于生物医学图像分割的卷积网络 (2015)
[2] Z. Zhou, Md. M. R. Siddiquee, N. Tajbakhsh 和 J. Liang, UNet++: 一种嵌套的 U-Net 架构用于医学图像分割 (2018)
[3] L. Chen, G. Papandreou, F. Schroff, H. Adam, 重新思考用于语义图像分割的膨胀卷积 (2017)
[4] L. Chen, Y. Zhu, G. Papandreou, F. Schroff, H. Adam, 用于语义图像分割的编码器-解码器与膨胀可分离卷积 (2018)
[5] R. Li, S. Zheng, C. Duan, C. Zhang, J. Su, P.M. Atkinson, 用于细分辨率遥感图像语义分割的多注意力网络 (2020)
[6] A. Chaurasia, E. Culurciello, LinkNet: 利用编码器表示进行高效的语义分割 (2017)
[7] T. Lin, P. Dollár, R. Girshick, K. He, B. Hariharan, S. Belongie, 用于目标检测的特征金字塔网络 (2017)
[8] H. Zhao, J. Shi, X. Qi, X. Wang, J. Jia, 金字塔场景解析网络 (2016)
[9] H. Li, P. Xiong, J. An, L. Wang, 用于语义分割的金字塔注意力网络 (2018)
[10] K. Simonyan, A. Zisserman, 用于大规模图像识别的非常深的卷积网络 (2014)
[11] Kaiming He, Xiangyu Zhang, Shaoqing Ren, Jian Sun, 用于图像识别的深度残差学习 (2015)
[12] S. Xie, R. Girshick, P. Dollár, Z. Tu, K. He, 深度神经网络的聚合残差变换 (2016)
[13] J. Hu, L. Shen, S. Albanie, G. Sun, E. Wu, 压缩-激励网络 (2017)
[14] G. Huang, Z. Liu, L. van der Maaten, K. Q. Weinberger, 密集连接卷积网络 (2016)
[15] M. Tan, Q. V. Le, EfficientNet: 重新思考卷积神经网络的模型缩放 (2019)
[16] E. Xie, W. Wang, Z. Yu, A. Anandkumar, J. M. Alvarez, P. Luo, SegFormer: 基于 Transformers 的简单高效语义分割设计 (2021)