TowardsDataScience 2023 博客中文翻译(三百零一)

原文:TowardsDataScience

协议:CC BY-NC-SA 4.0

结构化您的云实例启动脚本

原文:towardsdatascience.com/structuring-your-cloud-instances-startup-scripts-2ce981825b8d

区分首次启动与重启

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Jake Teo

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

大多数机器学习任务通常在初步探索阶段后,会被打包成镜像并部署到本地或云服务器上。这将促进快速迭代,以建立支持 MLOps 管道运行的基础设施,涉及整个开发团队,包括数据科学家、数据、软件和云工程师等。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

示例图展示了机器学习任务典型的服务器部署(VM = 虚拟机)。图片由作者提供

启动脚本用于在云服务器实例启动时执行自动化配置或其他任务。这在 AWS EC2 中被称为 用户数据,在 Google Cloud Engine 中称为 启动脚本,在 Azure Virtual Machine 中称为 自定义脚本扩展。启动脚本中的内容可以包括安装、元数据设置、环境变量等。其主要目的是确保每个实例在启动时始终配置为准备好服务于内部或相邻服务的应用程序。

就像我们编写的所有脚本一样,我们应始终使其保持整洁、结构化和集中,以便可以将其作为模板重复使用。这将使您在管理项目中不同实例的多个应用程序时更加轻松。在接下来的部分中,我将展示如何做到这一点。

虽然后面的部分专门针对 AWS EC2 的用户数据,但可以很容易地将其适应于其他提供商,只需使用相同的概念。

1) 首次启动与重启启动脚本

在实例首次启动时使用启动脚本是相当直观的,但重启呢?如果我们使用的是按需实例,并且它们不用于生产环境(例如开发、测试、系统集成测试、用户验收测试),那么在开发人员不在工作时(例如周末或下班后)让它们运行是没有经济意义的。因此,它们会被安排在需要时关闭和重启。在打补丁时也有需要重启的情况。

在这些关闭期间,可能会有应用程序需要的元数据更新。因此,在重启后,这些数据应该被刷新,以反映最新的信息。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

实例首次启动和重启所需的示例。图片来源:作者。

从现在开始,用户数据可以在实例首次启动时进行配置,也可以在重启时进行配置。通常,这两种启动方式所需的配置并不相同,但问题在于我们只能将一个用户数据文件附加到每个实例上。那么,我们如何在同一个用户数据文件中区分它们呢?

多部分格式

如果我们只要求在实例首次启动时执行用户数据,脚本只需包含 shell 命令。然而,要使其在每次实例重启时也能执行,则需要一个 cloud-config 命令。这是一个单独的格式,因此 AWS 使用 MIME(多用途互联网邮件扩展)多部分格式来包含这两种信息。

Content-Type: multipart/mixed; boundary="//"
MIME-Version: 1.0

--//
Content-Type: text/cloud-config; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment; filename="cloud-config.txt"

#cloud-config
cloud_final_modules:
- [scripts-user, always]

--//
Content-Type: text/x-shellscript; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment; filename="userdata.txt"

# your script here

--//--

从上面可以看到 MIME 的定义,随后是云配置,其中 [scripts-user, always] 表示用户数据将在实例首次启动和随后的重启时执行。下一个格式是为 shell 命令量身定制的。

区分首次启动和重启

从技术上讲,AWS 没有用户数据配置来根据首次启动和重启分隔你的脚本。幸运的是,我们可以使用一些简单的脚本优雅地完成这一任务,如下面的伪代码所示。

Content-Type: multipart/mixed; boundary="//"
MIME-Version: 1.0

--//
Content-Type: text/cloud-config; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment; filename="cloud-config.txt"

#cloud-config
cloud_final_modules:
- [scripts-user, always]

--//
Content-Type: text/x-shellscript; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment; filename="userdata.txt"

#!/bin/bash

# --------------- define functions --------------- #

function install_docker() {
  # some installations
}

function create_dotenv() {
  # create .env file
}

function setup_docker_compose() {
  # setup docker-compose.yml
}

function launch_docker_compose() {
  # launch your container
}

# --------------- execute script --------------- #

if [ ! -e "STARTED" ]; then
  # on first launch
  install_docker
  create_dotenv
  setup_docker_compose
  launch_docker_compose
  touch "STARTED";
else
  # on restart
  create_dotenv
  setup_docker_compose
fi

--//--

首先,我们需要将脚本结构化为函数,以便它们可以在首次启动或重启时调用。你可以看到我定义了 install_dockercreate_dotenvsetup_docker_composelaunch_docker_compose 四个函数。应设置适当的参数,使其尽可能可重用。

其次,我们有一个简单的 if-else 语句,当 STARTED 文件不存在时,它将执行所有四个函数,并在末尾创建 STARTED 文件。在该实例重启时,由于 STARTED 文件存在,它将仅运行两个配置函数,而不是其他函数。

这很简单,对吧?下面是一个使用 Ubuntu 虚拟机进一步说明的实际示例。

Content-Type: multipart/mixed; boundary="//"
MIME-Version: 1.0

--//
Content-Type: text/cloud-config; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment; filename="cloud-config.txt"

#cloud-config
cloud_final_modules:
- [scripts-user, always]

--//
Content-Type: text/x-shellscript; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment; filename="userdata.txt"

#!/bin/bash

# --------------- define functions --------------- #

function install_docker() {
  # https://docs.docker.com/engine/install/ubuntu/
  sudo apt-get update;
  sudo apt-get install -y ca-certificates gnupg lsb-release;

  sudo mkdir -p /etc/apt/keyrings
  curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --yes --dearmor -o /etc/apt/keyrings/docker.gpg
  echo \
    "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
    $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
  sudo apt update
  sudo apt-get -y install docker-ce docker-ce-cli containerd.io docker-compose-plugin
}

function create_dotenv() {
  ENV=$(curl -s http://169.254.169.254/latest/meta-data/tags/instance/Env)
  cd $1
  rm -f .env

  # Calculate memory limit & reservation for docker container
  # 90% limit, 70% reserved
  total_memory=$(free -m | awk '/^Mem:/{print $2}')
  MEM_LIMIT=$(echo "$total_memory * 0.9" | bc)
  MEM_LIMIT=$(printf "%.0f" "$MEM_LIMIT")
  MEM_RES=$(echo "$total_memory * 0.7" | bc)
  MEM_RES=$(printf "%.0f" "$MEM_RES")
  echo "Memory limit: $MEM_LIMIT $MEM_RES MB"

  echo MEM_LIMIT=${MEM_LIMIT}M >> .env
  echo MEM_RES=${MEM_RES}M >> .env  
  echo ENV=$ENV >> .env
  echo -e "[INFO] dotenv created ==========\n"
}

function setup_docker_compose() {
  # pull docker-compose file
  CI_TOKEN="get from secrets-manager"
  curl --header "PRIVATE-TOKEN: $CI_TOKEN" "https://gitlab.com/api/v4/projects/${1}/repository/files/docker-compose.yml/raw?ref=main" -o ${2}docker-compose.yml

  # pull image
  AWS_ACCOUNT=$(curl -s http://169.254.169.254/latest/dynamic/instance-identity/document | jq -r .accountId)
  AWS_REGION=$(curl -s http://169.254.169.254/latest/meta-data/placement/region)
  aws ecr get-login-password --region $AWS_REGION | docker login --username AWS --password-stdin
  echo -e "[INFO] docker-compose downloaded & docker logged in ==========\n"
}

function launch_docker_compose() {
  docker compose pull
  docker compose up -d
  echo -e "[INFO] docker image pulled and up ==========\n"
}

# --------------- execute script --------------- #

PROJECTID=12345678
HOMEDIR=/home/ubuntu/

if [ ! -e "STARTED" ]; then
  # on first launch
  install_docker
  create_dotenv $HOMEDIR
  setup_docker_compose $PROJECTID $HOMEDIR
  launch_docker_compose
  touch "STARTED";
else
  # on restart
  create_dotenv $HOMEDIR
  setup_docker_compose $PROJECTID $HOMEDIR
fi

--//--

每个函数的简短描述已经提供。请注意参数的使用,使每个函数都可以重用。

  • install_docker():更新包管理器,安装基础库以及 docker 和 docker-compose。

  • create_dotenv():从实例元数据标签中获取环境的元数据,例如开发、暂存、生产,并将其放入.env文件中。

  • set_docker_compose():从源代码库中获取最新的docker-compose.yml文件,使用文件中的环境设置镜像标签,然后登录到容器注册表。

  • launch_docker_compose():将镜像部署为容器

2) 集中启动脚本与克服字符限制

用户数据的字符或大小限制分别为 16K 和 16KB。这在大多数使用案例中是一个合理的长度。然而,如果超过此限制,你可以轻松地将脚本存储在像 S3 桶这样的 Blob 存储中,并在用户数据中拉取并执行这些脚本。这也是通过中央存储更新所有用户数据脚本的首选方法。

Content-Type: multipart/mixed; boundary="//"
MIME-Version: 1.0

--//
Content-Type: text/cloud-config; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment; filename="cloud-config.txt"

#cloud-config
cloud_final_modules:
- [scripts-user, always]

--//
Content-Type: text/x-shellscript; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment; filename="userdata.txt"

#!/bin/bash

# --------------- define functions --------------- #

function install_aws_cli() {
  sudo apt-get update;
  sudo apt-get install -y curl unzip;

  sudo curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip";
  sudo unzip awscliv2.zip;
  sudo ./aws/install;
  rm -f awscliv2.zip; rm -rf aws;
}

function download_scripts() {
  # download template functions from S3
  aws s3 cp s3://<s3.bucket.name>/userdata_template.sh userdata_template.sh
  source userdata_template.sh
}

# --------------- execute script --------------- #

PROJECTID=12345678
HOMEDIR=/home/ubuntu/

cd $HOMEDIR
if [ ! -e "STARTED" ]; then
  # on first launch
  install_aws_cli
  download_scripts
  install_docker
  create_dotenv $HOMEDIR
  setup_docker_compose $PROJECTID $HOMEDIR
  launch_docker_compose
  touch "STARTED";
else
  # on restart
  download_scripts
  create_dotenv $HOMEDIR
  setup_docker_compose $PROJECTID $HOMEDIR
fi

我们可以将上述四个函数存储在名为userdata_template.sh的文件中,并将其放置在你选择的 S3 桶中。

要访问 S3 桶,我们需要确保 1) 实例在实例配置文件中具有读取该桶的相关权限,以及 2) 实例已安装aws-cli以便使用适当的命令从 S3 中拉取启动脚本。

有了这些,我们可以轻松下载脚本,source它以访问之前的函数,并根据需要执行它们。

3) 调试

如果用户数据没有按预期执行任务,你可以查看实例中的日志文件,以查看是否捕获到任何错误消息。日志文件位于/var/log/cloud-init-output.log

# print the last 100 lines of log file
tail -n 100 /var/log/cloud-init-output.log

如果你需要检查用户数据脚本本身,可以使用以下两种方法。

# print the user data script
curl -s http://169.254.169.254/latest/user-data

# the script itself is stored in this directory
cd /var/lib/cloud/instance/scripts

总结

就这样!希望你从虚拟机的首次启动和重启中学到一些优雅的结构化用户数据的技巧。希望你觉得这些技巧有用且直观。

参考资料

将你的机器学习项目与 MLOps 思维相结合进行结构化

原文:towardsdatascience.com/structuring-your-machine-learning-project-with-mlops-in-mind-41a8d65987c9

MLOps 实践:项目结构化

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Chayma Zatout

· 发布于 Towards Data Science ·14 分钟阅读·2023 年 3 月 16 日

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片由 Priscilla Du Preez 提供,来源于 Unsplash

如果你希望将机器学习项目提升到一个新的水平,MLOps 是过程中的重要部分。本文将为你提供一个实用的教程,教你如何为 MLOps 结构化你的项目,以经典的手写数字分类问题为例。我们将逐步带你完成创建一个基本项目模板的过程,你可以用来组织自己的项目。通过这个教程,你将对 MLOps 原则有一个扎实的理解,并学会如何将它们应用到自己的项目中。然而,如果你对 MLOps 不熟悉,我们建议你先从我的 适合初学者的教程 开始,以便快速上手。让我们开始吧,将你的机器学习项目提升到一个新水平!

目录:

· 1. 介绍

· 2. MLOps

∘ 2.1. 业务问题

∘ 2.2. 数据工程

∘ 2.3. 机器学习模型工程

∘ 2.4. 代码工程

· 3. 项目结构

∘ 3.1. Cookiecutter 数据科学

· 4. MLOps 项目结构

∘ 4.1. 开始一个新的 MLOps 项目

∘ 4.2. 使用 MLOps 项目模板进行手写数字分类

∘ 4.3. 如何运行你的项目?

· 5. 结论

我的 MLOps 教程:

[我会在发布相关文章时更新此列表]

1. 介绍

在之前的教程中,我们将 MLOps 定义为一种以高效、优化和有序的方式设计、构建和部署机器学习模型的方法。这是通过结合一组技术、实践和工具来实现的,这些技术、实践和工具通常在 MLOps 生命周期的背景下进行讨论。

在 MLOps 生命周期中,了解问题后的第一步是构建您的项目。这通常通过使用模板完成,无论是公司模板、公共模板还是您自己的模板,如我们将在本教程中看到的那样。

在本教程中,我们将以手写数字分类为例。 在之前的教程中,我为 MNIST 分类创建了一个 Github 仓库,项目结构如下:

MNIST_classification
├── dataset_scripts
│   ├── construct_dataset_csv.py
│   ├── construct_dataset_folders.py
│   ├── describe_dataset_csv.py
│   ├── explore_dataset_idx.py
│   └── README.md
├── main_classification_convnet.py
├── main_classification_onehot.py
├── main_classification_single_output.py
├── .gitignore
└── README.md

项目文件夹包括“dataset_scripts”文件夹,该文件夹包含用于操作原始 IDX 格式数据集的脚本(有关更多信息,您可以查看我之前的教程“如何轻松探索您的 IDX 数据集”),用于训练三种不同类型模型的 Python 脚本,一个.gitignore 文件,以及一个 README 文件。由于该项目结构是为教程目的设计的,因此它非常简单。在本教程中,我将介绍我在 MLOps 项目中的项目结构。请注意,如果您想了解有关模型和训练的编程细节,您可以随时参考我的教程“神经网络简要介绍:分类问题”。

2. MLOps

机器学习过程中的不同步骤在 MLOps 工作流程中进行了概述,该工作流程包括业务问题、数据工程、机器学习模型工程和代码工程。在本节中,我们将探讨如何实现每一步。然而,由于我们正在解决的问题(手写数字分类)不需要某些步骤,我们不会深入讨论这些步骤。我们将重点关注用绿色突出显示的步骤(见下图)。剩余步骤将在未来的教程中覆盖。如果你想了解更多关于 MLOps 工作流程的内容,你可以查看我的入门友好教程

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

MLOps 工作流程。

2.1. 业务问题

本教程中解决的问题是手写数字分类,这是一个多类分类任务。具体而言,给定一个手写数字的输入图像(范围从 0 到 9),模型需要识别数字并输出其对应的标签。

AI 画布包含以下组件:任务描述、预测(模型输出)、判断、行动、结果、训练、输入、反馈和模型对问题的影响。对于当前的手写数字分类问题,我们的 AI 画布将按如下方式构建和填写:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

用于手写数字分类的 AI 画布。

2.2. 数据工程

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

数据工程

数据工程涵盖了各种任务,如数据摄取、探索和验证、清理、标注和拆分。在这个项目中,我们执行了以下数据工程任务:

  • 数据摄取: 我们从其官方网站下载了 MNIST 数据集的原始格式,并将其转换为 CSV 文件。

  • 数据探索和验证: 我们可视化了一些数据集中的图像,并展示了一些洞见。

  • 数据清理: 数据集已经很干净,无需进一步清理。

  • 数据标注: 数据集已经标注完毕,因此不需要额外的标注。

  • 数据拆分: 数据集已经被拆分为训练集和测试集。我们将从训练集中提取验证集。

值得注意的是,这个项目涉及的相对简单的数据工程过程,因为数据集已经准备和处理好了。然而,我们将在未来的文章中探讨更复杂的例子。

2.3. 机器学习模型工程

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

机器学习模型工程

机器学习模型工程是 MLOps 工作流程中的第三步。它包括各种任务,如模型训练、评估、测试和打包。在这个项目中,我们执行了以下机器学习模型工程任务:

  • 模型训练:在特征工程中,我们使用了数据缩放(将像素缩放到[0,1]范围内)、数据重塑(将图像表示为 1D 向量或 2D 矩阵)和数据编码(独热编码)。在模型工程中,我们实现了两种不同类型的模型并应用了超参数调优。

  • 模型评估:除了准确率,我们还使用了召回率、精确度和 F1 分数等其他评估指标,以确保模型符合 AI 画布中描述的业务目标(结果)。

  • 模型测试:在评估模型之后,我们在两种不同类型的数据上进行了测试:第一种是 MNIST 数据集的测试集,第二种是从应用程序生成的一些手写数字图像。

  • 模型打包和版本控制将在下一教程中讨论,我们将更详细地介绍机器学习管道。

如果你想了解更多编程细节,可以随时查看我之前的教程

2.4. 代码工程

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

代码工程

在代码工程步骤中,选择的模型会被部署到应用中,其性能需要被监控和记录。在部署模型之前,需要仔细选择服务模式和部署策略。部署后,需要管理和维护其行为,以确保其正常运行。尽管这一部分在本教程中没有详细说明,但我计划在不久的将来专门写一篇文章。

3. 项目结构

现在我们已经突出展示了在手写数字分类中应用的不同 MLOps 步骤,接下来让我们着手结构化项目,以满足项目需求,同时考虑这些步骤。为此,我将首先介绍一个著名的项目结构,然后展示我的 MLOps 项目结构模板。该模板将在我们添加更多组件时进行更新。

但为什么正确结构化你的机器学习项目很重要呢?好吧,有几个好处:

  • 良好的透明度: 组织有序的项目不仅对你自己,而且对他人也更易于理解。

  • 简单的维护: 结构良好的项目更易于维护和更新,从而节省时间和精力。

  • 提高效率: 清晰的计划减少了浪费的时间,最小化了偏离方向或丢失重要信息的风险。

  • 良好的可复现性和可重用性: 一个良好的项目结构确保项目结果可以轻松复现,并且其组件可以重用。

  • 便捷的协作: 当项目组织得清晰且逻辑性强时,其他人更容易理解和参与。

总之,正确构建机器学习项目的结构可以带来更大的透明度、效率、可维护性和协作。

3.1. Cookiecutter 数据科学

正如本文之前提到的,在编写任何代码之前,我们首先需要定义项目结构。这可以通过使用项目结构模板来实现。模板可以是公司为响应公司/项目需求而制定的公司模板,也可以是一个团体或个人创建并发布的公共模板,或者是您自己感觉舒适的自定义模板。

在该领域最著名的项目结构之一是Cookiecutter 数据科学,其结构如下:

一种逻辑合理、标准化但灵活的项目结构,用于进行和共享数据科学工作。

您可以在下面找到此模板的项目结构,以及每个文件的描述:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

4. MLOps 项目结构

现在我们已经解释了 MLOps 工作流程的不同步骤是如何执行的,让我们定义一个与 MLOps 工作流程对齐的项目结构模板。Cookiecutter MLOps 模板基于我们之前介绍的 Cookiecutter 数据科学模板。与 Cookiecutter 数据科学类似,我的 Cookiecutter MLOps 模板包括 LICENSE、README、Makefile 和 requirements 文件;以及 docs、models、notebooks、references、reports、visualization 和 source 文件夹。然而,新增了一个文件夹 (configs),并且 source 和 visualization 文件夹得到了增强。

MLOps 项目结构模板具有以下结构:

{{ cookiecutter.repo_name }}/
├── LICENSE
├── README.md
├── Makefile        # Makefile with commands like `make data` or `make train`
├── configs         # Config files (models and training hyperparameters)
│   └── model1.yaml
│
├── data
│   ├── external    # Data from third party sources.
│   ├── interim     # Intermediate data that has been transformed.
│   ├── processed   # The final, canonical data sets for modeling.
│   └── raw         # The original, immutable data dump.
│
├── docs            # Project documentation.
│
├── models          # Trained and serialized models.
│
├── notebooks       # Jupyter notebooks.
│
├── references      # Data dictionaries, manuals, and all other explanatory# materials.
│
├── reports         # Generated analysis as HTML, PDF, LaTeX, etc.
│   └── figures     # Generated graphics and figures to be used in reporting.
│
├── requirements.txt # The requirements file for reproducing the environment.
└── src              # Source code for use in this project.
    ├── __init__.py  # Makes src a Python module.
    │
    ├── data         # Data engineering scripts.
    │   ├── build_features.py
    │   ├── cleaning.py
    │   ├── ingestion.py
    │   ├── labeling.py
    │   ├── splitting.py
    │   └── validation.py
    │
    ├── models       # ML model engineering (a folder for each model).
    │   └── model1
    │       ├── dataloader.py
    │       ├── hyperparameters_tuning.py
    │       ├── model.py
    │       ├── predict.py
    │       ├── preprocessing.py
    │       └── train.py
    │
    └── visualization # Scripts to create exploratory and results# oriented visualizations.
        ├── evaluation.py
        └── exploration.py

configs 文件夹包含所有配置文件,例如模型超参数。

data 文件夹(src 的子文件夹)包括以下文件:

  • ingestion.py: 用于收集数据。如果需要创建备份、保护私人信息或创建元数据目录,最好在这里完成。

  • cleaning.py: 用于通过减少离群值/噪声、处理缺失值等来清理数据。

  • labeling.py: 如果需要,使用该文件对数据进行标注。

  • splitting.py: 用于将数据分为测试集和训练集。

  • validation.py: 用于验证数据(以确保其准备好进行训练)。

  • build_features.py: 该文件已移动到此文件夹,因为构建特征意味着将数据集组织成特定结构。

models 文件夹(src 的子文件夹)中,每个模型的脚本都在模型的文件夹中组织,包括:

  • model.py: 用于定义模型架构。

  • dataloader.py: 用于加载数据,以供模型使用。

  • preprocessing.py: 用于在将数据输入模型之前进行预处理。

  • train.py: 用于训练模型。

  • hyperparameters_tuning.py: 用于调整模型和/或训练超参数。

  • predict.py:用于对随机图像进行预测(不是来自数据集)。

可视化 文件夹包括以下内容:

  • exploration.py:此文件包括在数据工程过程中用于可视化数据的函数。

  • evaluation.py:此文件包括用于可视化训练结果的函数。

这是 MLOps 模板,有一些重要的注意事项需要考虑:

  • 这是一个基本模板,因此根据你的项目需求,可以删除或添加一些文件和文件夹。

  • 一些预处理函数可以在所有模型中使用,因此可以创建一个单独的预处理文件,并将其移动到数据文件夹中,以避免函数重复。然而,建议将预处理文件分开,以提高模型的可重用性,并防止未来潜在的问题。

  • 在预测脚本中,假设数据来自应用程序而不是数据集,因此可能需要额外的预处理步骤。

4.1. 启动一个新的 MLOps 项目

如果你想使用此模板启动你的机器学习项目,你可以使用GitHub 模板或使用Cookiecutter 模板,如下所示:

  • 要使用GitHub 模板,首先,你需要访问这里的模板页面。然后,点击绿色的按钮‘使用此模板’,你将需要选择是‘创建一个新仓库’还是‘在代码空间中打开’:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

GitHub 模板

  • 要使用Cookiecutter 模板,你首先需要安装 Cookiecutter,使用:
pip install cookiecutter

或者:

conda config --add channels conda-forge
conda install cookiecutter

然后在命令行中运行此命令:

cookiecutter https://github.com/Chim-SO/cookiecutter-mlops

这里是手写数字分类的示例配置,你可以通过填写所需的参数来进行自定义。按下 Enter 键将保留你不想更改的任何参数的默认值:

project_name [project_name]: MLOps_MLflow_mnist_classification

repo_name [mlops_mlflow_mnist_classification]: 

author_name [Your name (or your organization/company/team)]: Chim SO

description [A short description of the project.]: MNIST classification

Select open_source_license:
1 - MIT
2 - BSD-3-Clause
3 - No license file
Choose from 1, 2, 3 [1]: 1

s3_bucket [[OPTIONAL] your-bucket-for-syncing-data (do not include 's3://')]: 

aws_profile [default]:

Select python_interpreter:
1 - python3
2 - python
Choose from 1, 2 [1]:

4.2. 使用 MLOps 项目模板进行手写数字分类

在第二部分,我们讨论了手写数字分类任务中 MLOps 工作流的不同步骤。使用 MLOps 模板实现该管道将导致以下项目结构:

MLOps_MLflow_mnist_classification
├── configs
│   ├── cnnbased.yaml
│   └── singleoutput.yaml
├── data
│   ├── external
│   │   └── test
│   │       ├── 0_0.png
│   │       ├── 1_0.png
│   │       ├── 1_1.png
│   │       ├── 3_1.png
│   │       ├── 5_1.png
│   │       ├── 7_0.png
│   │       └── 8_0.png
│   ├── interim
│   ├── processed
│   │   ├── test.csv
│   │   └── train.csv
│   └── raw
│       ├── test_images.gz
│       ├── test_labels.gz
│       ├── train_images.gz
│       └── train_labels.gz
├── LICENSE
├── Makefile
├── MLproject
├── mlruns
├── models
├── README.md
├── requirements.txt
└── src
    ├── data
    │   ├── build_features.py
    │   ├── dataloader.py
    │   └── ingestion.py
    ├── models
    │   ├── cnnbased
    │   │   ├── hyperparameters_tuning.py
    │   │   ├── model.py
    │   │   ├── predict.py
    │   │   ├── preprocessing.py
    │   │   └── train.py
    │   └── singleoutput
    │       ├── hyperparameters_tuning.py
    │       ├── model.py
    │       ├── predict.py
    │       ├── preprocessing.py
    │       └── train.py
    └── visualization
        ├── evaluation.py
        └── exploration.py

由于我们已经描述了每个文件和文件夹的内容,现在我将重点介绍一些可能有点模糊的最重要步骤。

  • configs 文件夹包含两个配置文件,每个模型一个。例如,singleoutput.yaml 文件包括模型配置、训练参数、日志参数(将在下一个教程中讨论)以及模型调优参数。
# Data parameters
data:
  dataset_path : 'data/processed/'

# Model parameters
model:
  name: 'singleoutput'
  num_units: 224
  num_layers: 5
  activation_function : 'sigmoid'

# Training parameters
training:
  batch_size: 128
  num_epochs: 200
  loss_function: 'mae'
  metric: 'mse'

# Logging and output parameters
mlflow:
  mlruns_path: 'file:models/mlruns'
  experiment_name: 'singleOutput'

# Tuning
hyperparameter_tuning:
  num_layers: [3, 5]
  num_units: [16, 64, 224]
  activation_function: ['relu', 'sigmoid']
  batch_size: [128, 256]
  loss_function: ['mae']
  metric: ['mse']
  num_epochs: [200]
  • 使用src/data/ingestion.py,数据首先被下载并存储在data/raw/中。然后,使用src/data/build_features.py将其转换为记录结构,并直接存储到data/processed中。

  • data/external文件夹中,我添加了一个test子文件夹,其中包括一些随机的手写数字图像。这些图像将由predict.py脚本用于测试训练模型对新、未见数据的预测。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • 对于这个示例,data/interim 文件夹是空的,因为数据处理管道中没有中间步骤。

  • 由于数据集是经典数据集,数据加载器被移动到了src/data/,而不是为每个模型重复使用。

  • src/models/<model>/predict.py脚本概述了预测随机图像类别的管道。与用于训练模型的预处理管道(包括调整大小和缩放)不同,预测管道首先对图像进行裁剪,反转像素,然后调整大小和缩放。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

随机图像的数据预处理管道。

  • MLproject文件和mlruns文件夹由 MLflow 库使用,MLflow 是一个用于管理机器学习管道的平台。下一篇文章将详细介绍这个主题,所以如果你不熟悉它,也不用担心。

4.3. 如何运行你的项目?

执行 Python 项目有几种方法:交互式运行(逐行执行),批处理运行(安排定时任务或使用作业调度器),容器化运行(使用 Docker 或 Kubernetes),自动化运行(例如使用 MLflow),或分布式运行(使用像 Apache Spark 这样的分布式计算框架)。由于这不是本文的主要内容,我们将使用最简单的方法:从项目目录执行这些命令。

python src/data/ingestion.py -r data/raw/ # Download data
python src/data/build_features.py -r data/raw/ -p data/processed/ # Create csv files
python -m src.models.cnnbased.train -c configs/cnnbased.yaml # Train CNN model

5. 结论

在这篇文章中,我们提供了一个 MLOps 项目结构模板,并应用于手写数字分类问题。我们展示了如何将 MLOps 工作流应用于解决这个问题,并制定了一个你可以作为Cookiecutter 项目GitHub 模板使用的项目结构模板。如果你觉得这个模板有帮助,请在 GitHub 上给它一个星标,以便其他人也能发现。如果你是 MLOps 的新手,可以阅读我的初学者友好教程。

在接下来的文章中,我们将继续使用这个示例来覆盖所有 MLOps 工作流和原则。我会写更多关于 MLOps 及其各种技术的教程,并提供示例,请继续关注。

感谢阅读这篇文章。你可以在我的GitHub 个人资料中找到示例项目。如果你有任何问题或建议,请随时留言。

图像来源

文章中所有未在标题中提到来源的图像和图表均为作者提供。

使用分布式随机森林研究美国性别工资差距

原文:towardsdatascience.com/studying-the-gender-wage-gap-in-the-us-using-distributional-random-forests-ec4c2a69abf0?source=collection_archive---------6-----------------------#2023-02-18

分布式随机森林(DRF)的真实数据分析示例

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Jeffrey Näf

·

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片由 Ehimetalor Akhere Unuabona 拍摄,发布于 Unsplash

在之前的两篇文章中,我解释了分布式随机森林(DRFs),这是一种能够估计条件分布的随机森林,以及一种方法的扩展,它允许进行不确定性量化,如置信区间等。这里我展示了一个实际应用的例子,数据来自 2018 年美国社区调查,由美国人口普查局提供。 在第一篇DRF 论文中,我们获得了来自 2018 年美国社区调查的大约 100 万名全职员工的数据,从中提取了薪资信息和所有可能与薪资相关的协变量。这些数据非常适合用来实验 DRF 这种方法(实际上我们将在本分析中只使用一个微小的子集)。

当研究原始时薪数据时,两个性别之间存在一致的差距,即男性往往赚得更多。一个有趣的问题是,男性(G=1)和女性(G=0)之间观察到的时薪差距(W)是否仅仅由于性别,还是可以通过一些其他混杂变量X来解释,这些变量受性别影响并反过来影响工资。也就是说,我们想研究与以下因果图中的粗体箭头对应的效应大小:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

假设因果图,G=性别,W=工资,X是混杂变量

例如,假设X仅包括职业,并且女性倾向于选择不涉及高 monetary 奖励的职业,如医生、护士或教师,而男性则倾向于从事专业赌博工作,时薪极高。如果仅凭这一点来解释性别之间的时薪差异,我们仍然会看到直接观察到的时薪差距。然而,如果我们将职业固定为X的医生,并比较这两种工资分布,那么任何统计上显著的差异只能来自性别本身。

我们关注于两阶段分析:

  • 我们将X固定为一个特定值,并比较在X*=x固定的协变量下两个组的工资分布。这从两个方面来看都很有趣:首先,如果X确实包含所有影响工资且与性别相关的其他因素,那么固定X**=x并查看两性工资,就意味着我们真正观察到了性别对工资的影响。其次,它允许对具有给定特征x*的个体进行整个工资分布的预测。

  • 我们使用上面假设的因果图和因果规则,通过 DRF 估计一个反事实分布:女性工资的分布,假如她们被当作男性来设定工资。如果X包含所有相关协变量,并且不存在性别工资差距,这个分布应该与男性的工资分布相同(忽略统计随机性)。

这篇文章是几个人工作的最终成果:代码和数据集来自原始的DRF 仓库,然后与我们新论文中在arXiv上开发的方法结合,这篇论文由Corinne Emenegger共同撰写。

在继续之前,我想指出这仅仅是一个用来说明 DRF 使用的例子。我并不打算在这里做出任何严肃的(因果)声明,因为分析肯定在某些方面存在缺陷,我们下面假设的因果图肯定是错误的。此外,我们只使用了可用数据中的一小部分。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

此外,请注意代码运行速度较慢。这是因为,虽然 DRF 本身是用 C 语言编写的,但用于置信区间的重复拟合目前是用 R 实现的。

话虽如此,让我们深入了解。接下来,除非另有说明,所有图片均由作者提供。

数据

来自 2018 年 1 年期美国社区调查的 PUMS(公共使用微数据区域)数据来自美国人口普查局 API。该调查每年发送给约 350 万人,旨在提供比每十年进行一次的官方普查更为最新的数据。2018 年的数据集包含大约 300 万条匿名数据点,涵盖了 51 个州和哥伦比亚特区。对于上面链接的 DRF 论文,我们仅提取了可能与工资相关的变量子集,如个人的性别、年龄、种族、婚姻状况、教育水平和英语水平。

预处理的数据可以在这里找到。我们首先进行一些进一步的清理:

##Further data cleaning ##

which = rep(TRUE, nrow(wage))
which = which & (wage$age >= 17)
which = which & (wage$weeks_worked > 48)
which = which & (wage$hours_worked > 16)
which = which & (wage$employment_status == 'employed')
which = which & (wage$employer != 'self-employed')
which[is.na(which)] = FALSE

data = wage[which, ]
sum(is.na(data))
colSums(is.na(data))
rownames(data) = 1:nrow(data)
#data = na.omit(data)

data$log_wage = log(data$salary / (data$weeks_worked * data$hours_worked))

## Prepare data and fit drf
## Define X and Y
X = data[,c(
  'age',
  'race',
  'hispanic_origin',
  'citizenship',
  'nativity', 
  'marital',
  'family_size',
  'children',
  'education_level',
  'english_level',
  'economic_region'
)]
X$occupation = unlist(lapply(as.character(data$occupation), function(s){return(substr(s, 1, 2))}))
X$occupation = as.factor(X$occupation)
X$industry = unlist(lapply(as.character(data$industry), function(s){return(substr(s, 1, 2))}))
X$industry[X$industry %in% c('32', '33', '3M')] = '31'
X$industry[X$industry %in% c('42')] = '41'
X$industry[X$industry %in% c('45', '4M')] = '44'
X$industry[X$industry %in% c('49')] = '48'
X$industry[X$industry %in% c('92')] = '91'
X$industry = as.factor(X$industry)
X=dummy_cols(X, remove_selected_columns = TRUE)
X = as.matrix(X)

Y = data[,c('sex', 'log_wage')]
Y$sex = (Y$sex == 'male')
Y = as.matrix(Y)

实际上,这些观察值远远超过我们需要的,我们在此分析中随机抽样了 4’000 个训练数据点。

train_idx = sample(1:nrow(data), 4000, replace = FALSE)

## Focus on training data
Ytrain=Y[train_idx,]
Xtrain=X[train_idx,]

再次说明,这是因为它只是一个示例——实际上,你会希望获取尽可能多的数据点。这 4’000 个数据点的两性工资估计密度绘制在图 1 中,使用了以下代码:

## Plot the test data without adjustment
plotdfunadj = data[train_idx, ]
plotdfunadj$weight=1
plotdfunadj$plotweight[plotdfunadj$sex=='female'] = plotdfunadj$weight[plotdfunadj$sex=='female']/sum(plotdfunadj$weight[plotdfunadj$sex=='female'])
plotdfunadj$plotweight[plotdfunadj$sex=='male'] = plotdfunadj$weight[plotdfunadj$sex=='male']/sum(plotdfunadj$weight[plotdfunadj$sex=='male'])

#pooled data
ggplot(plotdfunadj, aes(log_wage)) +
  geom_density(adjust=2.5, alpha = 0.3, show.legend = TRUE,  aes(fill=sex, weight=plotweight)) +
  theme_light()+
  scale_fill_discrete(name = "gender", labels = c('female', "male"))+
  theme(legend.position = c(0.83, 0.66),
        legend.text=element_text(size=18),
        legend.title=element_text(size=20),
        legend.background = element_rect(fill=alpha('white', 0.5)),
        axis.text.x = element_text(size=14),
        axis.text.y = element_text(size=14),
        axis.title.x = element_text(size=19),
        axis.title.y = element_text(size=19))+
  labs(x='log(hourly_wage)')

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

每小时工资的(无条件)原始对数的估计密度

计算两者工资的百分比中位差,即

(男性中位工资 - 女性中位工资)/(女性中位工资)*100,

我们获得了约 18%的结果。也就是说,在未经调整的数据中,男性的中位薪资比女性高 18%(!)

## Median Difference before adjustment!
quantile_maleunadj = wtd.quantile(x=plotdfunadj$log_wage, weights=plotdfunadj$plotweight*(plotdfunadj$sex=='male'), normwt=TRUE, probs=0.5)
quantile_femaleunadj = wtd.quantile(x=plotdfunadj$log_wage, weights=plotdfunadj$plotweight*(plotdfunadj$sex=='female'), normwt=TRUE, probs=0.5)
(1-exp(quantile_femaleunadj)/exp(quantile_maleunadj))

分析

问题现在变成了这是否真的“不公平”。也就是说,我们假设上述因果图,其中性别(G)影响工资(W),以及协变量X,这些协变量反过来影响W。我们想知道的是性别是否直接影响工资(粗体箭头)。也就是说,如果一个女性和一个具有完全相同特征的男性X*=*x获得相同的工资,还是因为她的性别她获得了更少的工资。

我们将在两种情况下进行研究。第一种情况是将X*=*x保持不变,并使用在早期文章中解释的机制。直观地说,如果我们固定性别之外可能影响工资的所有其他协变量,然后比较这两种工资分布,那么任何观察到的差异必须仅由工资造成。

第二种方法尝试对所有可能的X值量化这种差异。通过计算反事实分布来实现这一点。

W(男性,X(女性))。

这个量是一个男性如果具有女性的特征时得到的反事实工资。也就是说,我们询问一个女性在像男性一样对待时的工资。

请注意,这假设了上述因果图是正确的。特别是,它假设X捕捉到除了性别之外的所有相关因素,这些因素会决定工资。可能情况并非如此,因此在本文开头的免责声明。

研究条件分布差异

接下来,我们将x固定到一个任意点:

i<-47

# Important: Test point needs to be a matrix
test_point<-X[i,, drop=F]

以下图片展示了一些包含在该测试点x中的值——我们正在查看具有高中学历、已婚且有 1 个孩子的保育员。使用 DRF,我们可以估计并绘制条件于X*=*x的两个组的密度:

# Load all relevant functions (the CIdrf.R file can be found at the end of this 
# article
source('CIdrf.R')

# predict with the new framework
DRF = predictdrf(drf_fit, x=x)
weights <- DRF$weights

## Conditional Density Plotting
plotdfx = data[train_idx, ]

propensity = sum(weights[plotdfx$sex=='female'])
plotdfx$plotweight = 0
plotdfx$plotweight[plotdfx$sex=='female'] = weights[plotdfx$sex=='female']/propensity
plotdfx$plotweight[plotdfx$sex=='male'] = weights[plotdfx$sex=='male']/(1-propensity)

gg = ggplot(plotdfx, aes(log_wage)) +
  geom_density(adjust=5, alpha = 0.3, show.legend=TRUE,  aes(fill=sex, weight=plotweight)) +
  labs(x='log(hourly wage)')+
  theme_light()+
  scale_fill_discrete(name = "gender", labels = c(sprintf("F: %g%%", round(100*propensity, 1)), sprintf("M: %g%%", round(100*(1-propensity), 1))))+
  theme(legend.position = c(0.9, 0.65),
        legend.text=element_text(size=18),
        legend.title=element_text(size=20),
        legend.background = element_rect(fill=alpha('white', 0)),
        axis.text.x = element_text(size=14),
        axis.text.y = element_text(size=14),
        axis.title.x = element_text(size=19),
        axis.title.y = element_text(size=19))+
  annotate("text", x=-1, y=Inf, hjust=0, vjust=1, size=5, label = point_description(data[i,]))
plot(gg)

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

给定X=x的两个性别的对数(每小时工资)密度的估计。此图的代码可以在文章末尾找到。

在这个图中,即使在固定的x情况下,也明显存在工资差异(记住,在这种情况下所有假定的混杂因素都被固定,因此我们实际上只是直接比较工资)。使用 DRF,我们现在估计并测试中位差异

## Getting the respective weights
weightsmale<-weights*(Ytrain[, "sex"]==1)/sum(weights*(Ytrain[, "sex"]==1))
weightsfemale<-weights*(Ytrain[, "sex"]==0)/sum(weights*(Ytrain[, "sex"]==0))

## Choosing alpha:
alpha<-0.05

# Step 1: Doing Median comparison for fixed x

quantile_male = wtd.quantile(x=data$log_wage[train_idx], weights=matrix(weightsmale), normwt=TRUE, probs=0.5)
quantile_female = wtd.quantile(x=data$log_wage[train_idx], weights=matrix(weightsfemale), normwt=TRUE, probs=0.5)

(medianx<-unname(1-exp(quantile_female)/exp(quantile_male)))

mediandist <- sapply(DRF$weightsb, function(wb) {

  wbmale<-wb*(Ytrain[, "sex"]==1)/sum(wb*(Ytrain[, "sex"]==1))
  wbfemale<-wb*(Ytrain[, "sex"]==0)/sum(wb*(Ytrain[, "sex"]==0))

  quantile_maleb = wtd.quantile(x=data$log_wage[train_idx], weights=matrix(wbmale), normwt=TRUE, probs=0.5)
  quantile_femaleb = wtd.quantile(x=data$log_wage[train_idx], weights=matrix(wbfemale), normwt=TRUE, probs=0.5)

  return( unname(1-exp(quantile_femaleb)/exp(quantile_maleb)) ) 
})

varx<-var(mediandist)

## Use Gaussian CI:
(upper<-medianx + qnorm(1-alpha/2)*sqrt(varx))
(lower<-medianx - qnorm(1-alpha/2)*sqrt(varx)) 

这给出了中位差异的置信区间。

(0.06, 0.40) 或 (6%, 40%)

这个区间非常明显地不包含零,因此中位差异确实是显著的。

使用 Witobj 函数,我们可以更清楚地显示这种差异。

 Witobj<-Witdrf(drf_fit, x=test_point, groupingvar="sex", alpha=0.05)

hatmun<-function(y,Witobj){

  c<-Witobj$c
  k_Y<-Witobj$k_Y
  Y<-Witobj$Y
  weightsall1<-Witobj$weightsall1
  weightsall0<-Witobj$weightsall0
  Ky=t(kernelMatrix(k_Y, Y , y = y))

  out<-list()
  out$val <- tcrossprod(Ky, weightsall1  ) - tcrossprod(Ky, weightsall0  )
  out$upper<-  out$val+sqrt(c)
  out$lower<-  out$val-sqrt(c)

  return( out )

}

all<-hatmun(sort(Witobj$Y),Witobj)

plot(sort(Witobj$Y),all$val , type="l", col="darkblue", lwd=2, ylim=c(min(all$lower), max(all$upper)),
     xlab="log(wage)", ylab="witness function")
lines(sort(Witobj$Y),all$upper , type="l", col="darkgreen", lwd=2 )
lines(sort(Witobj$Y),all$lower , type="l", col="darkgreen", lwd=2 )
abline(h=0)

这导致了图示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

工资的条件见证函数的估计,男性减去女性的工资。

我们参考相关文章以获得对该概念的更详细解释。本质上,它可以被视为

在给定 x 的情况下,男性的工资对数的条件密度在给定 x 的情况下,女性的工资对数的条件密度

即,条件见证函数显示了一个组的密度高于另一个组的位置,而无需实际估计密度。在这个例子中,负值表示女性工资的密度在给定x的情况下高于男性工资的密度,正值表示女性工资的密度较低。由于我们已经估计了上述的条件密度,条件见证函数本身并不会增加太多信息。但它对于说明情况很有用。确实,我们看到它在开始时是负的,对于条件密度的女性工资高于条件密度的男性工资的值。相反,它在更大的值下变为正值,对于这些值,男性工资的条件密度高于女性工资。因此,关于两个密度的相关信息在见证函数图中总结:我们看到女性工资的密度在较低工资值时较高,而在较高工资值时较低,表明密度向左偏移,女性赚得更少!此外,我们还可以提供包含真实函数 95%的 95%置信区间(绿色),在所有 y 值上均匀分布。 (尽管实际上需要大量的数据才能使其有效)由于这个均匀置信区间在 2 到 2.5 左右的零线之间不包含,我们再次看到这两个分布之间的差异在统计上是显著的。

对特定的x进行条件化,使我们能够详细研究个体效应,并具有不确定性的概念。然而,研究整体效应也很有趣。我们将在下一节中通过估计反事实分布来实现这一点。

估计反事实分布

使用我们假设的因果图的因果性计算法则,可以推导出:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

即,我们寻找的反事实分布是通过对性别为女性的x,平均条件分布W | G=maleX*=*x获得的。

由于分布被给定为简单的权重,这可以通过以下方式轻松完成:DRF

## Add code

## Male is 1, Female is 0

# obtain all X from the female test population
Xtestf<-Xtest[Ytest[,"sex"]==0,]

# Obtain the conditional distribution of W | G=male, X=x, for x in the female
# population.

# These weights correspond to P(W, G=male | X=x  )
weightsf<-predictdrf(drf_fit, x=Xtestf)$weights*(Ytrain[, "sex"]==1)
weightsf<-weightsf/rowSums(weightsf)

# The counterfactual distribution is the average over those weights/distributions 
counterfactualw<-colMeans(weightsf)

这导致了以下的反事实密度估计:

plotdfc<-rbind(plotdfc, plotdfunadj[plotdfunadj$sex=='female',])
plotdfc$sex2<-c(rep(1, length(train_idx)), rep(0,nrow(plotdfunadj[plotdfunadj$sex=='female',])))

plotdfc$sex2<-factor(plotdfc$sex2)

#interventional distribution
ggplot(plotdfc, aes(log_wage)) +
  geom_density(adjust=2.5, alpha = 0.3, show.legend=TRUE,  aes(fill=sex2, weight=plotweight)) +
  theme_light()+
  scale_fill_discrete(name = "", labels = c("observed women's wages", "wages if treated as men"))+
  theme(legend.position = c(0.2, 0.98),
        legend.text=element_text(size=16),
        legend.title=element_text(size=20),
        legend.background = element_rect(fill=alpha('white', 0)),
        axis.text.x = element_text(size=14),
        axis.text.y = element_text(size=14),
        axis.title.x = element_text(size=19),
        axis.title.y = element_text(size=19))+
  labs(x='log(hourly wage)')

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这两个密度现在分别是红色的女性工资密度,以及如果女性被当作男性来设置工资的话的绿色-蓝绿色密度。显然,现在这些密度比之前更接近——调整了混杂因素使得性别薪酬差异变小。然而,中位数差异仍然

quantile_male = wtd.quantile(x=plotdfc$log_wage[plotdfc$sex2==1], weights=counterfactualw, normwt=TRUE, probs=0.5)
quantile_female = wtd.quantile(x=plotdfunadj$log_wage, weights=plotdfunadj$plotweight*(plotdfunadj$sex=='female'), normwt=TRUE, probs=0.5)
(1-exp(quantile_female)/exp(quantile_male)) 

0.11 或 11 百分比!

因此,如果我们的分析是正确的,那么 11%的薪资差异仍然可以归因于性别。换句话说,虽然我们将未调整数据中 18%的中位收入差异减少到 11%,但仍然存在实质性的差异,表明性别之间存在“非公平”的工资差距(至少如果X确实捕捉到了相关的混杂因素)。

结论

在这篇文章中,我们研究了如何将 DRF 应用于实际数据分析的一个例子。我们探讨了固定的x的情况,对于这种情况,本文讨论的方法允许构建不确定性度量,以及反事实量的分布。在这两种情况下,我们都看到在调整可用的混杂变量时,仍然存在实质性差异,在固定的x情况下尤其显著。

虽然我没有检查,但看到这个小实验的结果与更严肃的分析相比可能会很有趣。无论如何,我希望这篇文章展示了 DRF 如何在实际数据分析中使用。

额外代码

## Functions in CIdrf.R that is loaded above ##

drfCI <- function(X, Y, B, sampling = "binomial",...) {

### Function that uses DRF with subsampling to obtain confidence regions as
### as described in https://arxiv.org/pdf/2302.05761.pdf
### X: Matrix of predictors
### Y: Matrix of variables of interest
### B: Number of half-samples/mini-forests

  n <- dim(X)[1]

  # compute point estimator and DRF per halfsample S
  # weightsb: B times n matrix of weights
  DRFlist <- lapply(seq_len(B), function(b) {

    # half-sample index
    indexb <- if (sampling == "binomial") {
      seq_len(n)[as.logical(rbinom(n, size = 1, prob = 0.5))]
    } else {
      sample(seq_len(n), floor(n / 2), replace = FALSE)
    }

    ## Using refitting DRF on S
    DRFb <- 
      drf(X = X[indexb, , drop = F], Y = Y[indexb, , drop = F],
          ci.group.size = 1, ...)

    return(list(DRF = DRFb, indices = indexb))
  })

  return(list(DRFlist = DRFlist, X = X, Y = Y) )
}

predictdrf<- function(DRF, x, ...) {

### Function to predict from DRF with Confidence Bands
### DRF: DRF object
### x: Testpoint

  ntest <- nrow(x)
  n <- nrow(DRF$Y)

  ## extract the weights w^S(x)
  weightsb <- lapply(DRF$DRFlist, function(l) {

    weightsbfinal <- Matrix(0, nrow = ntest, ncol = n , sparse = TRUE)

    weightsbfinal[, l$indices] <- predict(l$DRF, x)$weights 

    return(weightsbfinal)
  })

  ## obtain the overall weights w
  weights<- Reduce("+", weightsb) / length(weightsb)

return(list(weights = weights, weightsb = weightsb ))
}

Witdrf<- function(DRF, x, groupingvar, alpha=0.05, ...){

### Function to calculate the conditional witness function with
### confidence bands from DRF
### DRF: DRF object
### x: Testpoint

  if (is.null(dim(x)) ){

  stop("x needs to have dim(x) > 0")
  }

  ntest <- nrow(x)
  n <- nrow(DRF$Y)
  coln<-colnames(DRF$Y)

  ## Collect w^S
  weightsb <- lapply(DRF$DRFlist, function(l) {

    weightsbfinal <- Matrix(0, nrow = ntest, ncol = n , sparse = TRUE)

    weightsbfinal[, l$indices] <- predict(l$DRF, x)$weights 

    return(weightsbfinal)
  })

  ## Obtain w
  weightsall <- Reduce("+", weightsb) / length(weightsb)

  #weightsall0<-weightsall[, DRF$Y[, groupingvar]==0, drop=F]
  #weightsall1<-weightsall[,DRF$Y[, groupingvar]==1, drop=F]

  # Get the weights of the respective classes (need to standardize by propensity!)
  weightsall0<-weightsall*(DRF$Y[, groupingvar]==0)/sum(weightsall*(DRF$Y[, groupingvar]==0))
  weightsall1<-weightsall*(DRF$Y[, groupingvar]==1)/sum(weightsall*(DRF$Y[, groupingvar]==1))

  bandwidth_Y <- drf:::medianHeuristic(DRF$Y)
  k_Y <- rbfdot(sigma = bandwidth_Y)

  K<-kernelMatrix(k_Y, DRF$Y[,coln[coln!=groupingvar]], y = DRF$Y[,coln[coln!=groupingvar]])

  nulldist <- sapply(weightsb, function(wb){
    # iterate over class 1

    wb0<-wb*(DRF$Y[, groupingvar]==0)/sum(wb*(DRF$Y[, groupingvar]==0))
    wb1<-wb*(DRF$Y[, groupingvar]==1)/sum(wb*(DRF$Y[, groupingvar]==1))

    diag( ( wb0-weightsall0 - (wb1-weightsall1) )%*%K%*%t( wb0-weightsall0 - (wb1-weightsall1) )  )

  })

  # Choose the right quantile
  c<-quantile(nulldist, 1-alpha)

  return(list(c=c, k_Y=k_Y, Y=DRF$Y[,coln[coln!=groupingvar]], nulldist=nulldist, weightsall0=weightsall0, weightsall1=weightsall1))

}
### Code to generate plots

## Step 0: Choosing x

point_description = function(test_point){
  out = ''

  out = paste(out, 'job: ', test_point$occupation_description[1], sep='')
  out = paste(out, '\nindustry: ', test_point$industry_description[1], sep='')

  out = paste(out, '\neducation: ', test_point$education[1], sep='')
  out = paste(out, '\nemployer: ', test_point$employer[1], sep='')
  out = paste(out, '\nregion: ', test_point$economic_region[1], sep='')

  out = paste(out, '\nmarital: ', test_point$marital[1], sep='')
  out = paste(out, '\nfamily_size: ', test_point$family_size[1], sep='')
  out = paste(out, '\nchildren: ', test_point$children[1], sep='')

  out = paste(out, '\nnativity: ', test_point$nativity[1], sep='')
  out = paste(out, '\nhispanic: ', test_point$hispanic_origin[1], sep='')
  out = paste(out, '\nrace: ', test_point$race[1], sep='')
  out = paste(out, '\nage: ', test_point$age[1], sep='')

  return(out)
}

数据科学成功秘诀:你在大学里没有学到的 4 项关键技能

原文:towardsdatascience.com/succeeding-in-data-science-4-essential-skills-you-didnt-learn-in-university-1920815acef3

如何弥合学术界与数据科学领域就业之间的差距

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Tomer Gabay

·发表于 Towards Data Science ·6 分钟阅读·2023 年 5 月 11 日

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

krakenimages 提供的照片,来源于 Unsplash

自从从荷兰的一所大学毕业后,我在不同组织和公司担任了多个数据科学家的职位。让我和一些其他毕业生感到惊讶的是,大学所教的技能与成为一名有价值的数据科学员工所需的技能之间的差距。在这篇文章中,我想强调大学所教的技能和我发现对数据科学员工表现良好所需的四大技能差距。

我在每一节中都包含了有用的资源,以帮助你提升在该领域的技能,若你希望提高你的熟练程度。

构建扎实的数据科学项目

在大学里,通常会使用 Jupyter Notebook 进行“笔记本式”的工作。我在大学时设置的最稳固项目结构是从main.py文件调用不同的模块,并使用requirements.txt。但是使用setup.py?或者pyproject.toml?没有。

公司对数据科学项目的要求往往大相径庭。你的数据科学项目通常应该被构建成一个可安装的包,例如通过pip install <package_name>。有时需要一个requirements.txt,但至少应该在pyproject.toml中指定包的依赖(现在比setup.py更受欢迎,参见PEP-518)。

除了可以安装外,你的数据科学项目通常还应能够在云中作为基于 Docker 的 API 运行。通过使用 REST 请求调用你的 API,可以通过管道处理数据,甚至用机器学习模型进行预测。

如果你发现自己也主要是在以 ‘Notebook 风格’ 进行数据科学项目,或者你不熟悉如何构建一个可安装的 Python 项目,我建议阅读例如:

## 如何将你的 Python 项目转换为通过 pip 安装的包

一个带有现成模板的教程,描述了如何将 Python 项目转换为可以在…中使用的包。

towardsdatascience.com [## 如何启动任何专业的 Python 包项目。

包括测试自动化、在 ReadTheDocs 上创建文档以及发布到 PyPi。

medium.com](https://medium.com/mlearning-ai/how-to-start-any-professional-python-package-project-9f66538ebc2?source=post_page-----1920815acef3--------------------------------)

如果你能够构建一个可安装的包,但不确定如何将数据科学项目构建并运行为基于 Docker 的 API,请阅读:

## 在 AWS 上将机器学习模型部署为 API

逐步指南,构建模型 API、使用 Docker 容器化并通过 AWS Elastic 部署到网络。

towardsdatascience.com

在基于云的环境中工作

现在,几乎每家公司和组织都使用至少一些基于云的软件。目前,最受欢迎的有 Microsoft AzureGCPAWS。在我的大学里,我几乎没有使用这些云平台上提供的资源。然而,在工作中,数据科学家对云平台的知识几乎与他们的编程技能一样重要。以下是我作为数据科学家迄今为止需要完成的(云)工程任务的一个例子:

  • 在 Docker 容器中部署 Python 应用程序

  • 构建 CI/CD 管道

  • 在云中托管 Python 包作为工件

  • 构建数据管道

如果你对上述某个平台有一定的云知识,这在申请数据科学职位时已经是一个巨大的优势。每个平台都有自己的学习路线和证书来证明你在云计算方面的知识。每个平台都有不同角色的不同路线。以下是每个平台针对数据科学家的相关路线:

获得这样的证书无疑会让你作为(潜在的)数据科学员工更具价值。

与其他数据科学家合作

大学里的数据科学和现实生活中的数据科学之间最重要的区别之一是,在现实生活中,你必须持续不断地协作,而在大学里,通常只有少数几个团队项目贯穿整个本科或硕士阶段。

到目前为止,我工作过的每家公司和组织都使用了 scrumDevOps。我没有遇到过一次面试中对这两种方法的知识和/或经验没有加分的情况。除了使用 scrumDevOps,代码审查也是数据科学家工作中的关键任务。这意味着你既需要能够编写清晰易读的代码以供他人审查,也需要能够快速且批判性地评估他人的代码。你可以在下面的链接中找到一篇非常有趣的关于如何为代码审查提供建设性反馈的文章:

## 如何像人类一样进行代码审查(第一部分)

最近,我在阅读关于代码审查最佳实践的文章。我注意到这些文章专注于发现…

mtlynch.io

此外,你不能再用不明确的函数或变量名称,或不遵守 Python 的 PEP-8 命名规范 的对象来应付了。如果你想了解更多关于如何编写高质量代码的内容,可以参考下面的文章:

## 区分高级开发者和初级开发者的 6 个 Python 最佳实践

如何编写被视为来自经验丰富开发者的 Python 代码。

towardsdatascience.com

处理现实生活中的数据

在大学里,学生遇到的大多数数据集已经经过大量预处理。这些数据集的列通常具有有意义的名称,错误的条目已被删除,数据类型也已经正确配置。你在公司或组织中遇到的数据通常远不如大学里遇到的数据标准,除非你在一家真正的数据驱动(技术)公司工作。

我在工作中遇到的一些杂乱的真实数据示例:

  • 一些犯罪的数据,其中某些犯罪的实施日期在未来。

  • 没有任何命名规范的列名 [ name, Address, JobDescription, place_of_birth]

  • 重复的列具有不同的值 [ job_title JobTitle ]

  • 不同时区的 DateTime 值而不涉及时区问题。

由于真实数据比为学生准备的数据要复杂得多,作为数据科学家,你要准备好花费大部分时间与业务方沟通,询问列和数值的解释、数据清理以及合并来自不同来源的数据,而不是进行数据分析或构建机器学习模型。

欲了解更多数据清理的信息,例如:

## 数据清理的终极指南

当数据产生垃圾时

[towardsdatascience.com

如果你想在一些真实(杂乱)数据集上练习,这里有十个数据集供你练习数据清理技能!

总结

大学教授的数据科学技能可能不足以在数据科学员工职位上出色表现。作为在多个组织工作过的数据科学家,我注意到四个主要技能缺口需要解决:

  • 建立稳固的数据科学项目。

  • 在基于云的环境中工作。

  • 与其他数据科学家合作。

  • 处理真实数据。

利用本文中提到的资源,你可以帮助缓解在这些高度重视的领域中可能存在的知识或技能缺乏!

当然,每所大学和每个专业都有自己的数据科学课程,一些大学可能在为你准备成为数据科学员工方面做得更好或更差。此外,我毕业已经有几年了,因此也许大学的数据科学课程在云领域等方面已经有所改进,例如增加了(更多)与云相关的课程。

资源

稳固的 Python 项目 packaging.python.org/en/latest/tutorials/packaging-projects/

medium.com/r/url=https%3A%2F%2Ftowardsdatascience.com%2Fhow-to-convert-your-python-project-into-a-package-installable-through-pip-a2b36e8ace10

medium.com/mlearning-ai/how-to-start-any-professional-python-package-project-9f66538ebc2

towardsdatascience.com/deploy-a-machine-learning-model-as-an-api-on-aws-43e92d08d05b

基于云的环境 learn.microsoft.com/en-us/certifications/azure-data-scientist/

cloud.google.com/learn/certification/machine-learning-engineer

aws.amazon.com/certification/certified-data-analytics-specialty/

与其他数据科学家合作 www.scrum.org/resources/what-scrum-module

en.wikipedia.org/wiki/DevOp

mtlynch.io/human-code-reviews-1/

peps.python.org/pep-0008/

medium.com/towards-data-science/6-python-best-practices-that-distinguish-seniors-from-juniors-84199d4cac3c

与现实数据打交道 towardsdatascience.com/the-ultimate-guide-to-data-cleaning-3969843991d4

analyticsindiamag.com/10-datasets-for-data-cleaning-practice-for-beginners/

通过技术图示实现 ML 项目的成功

原文:towardsdatascience.com/success-in-ml-projects-through-technical-drawings-69dd8d2744a4

通过技术图示改进机器学习(ML)项目的工作流程和期望管理

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 本杰明·图勒

·发表于数据科学前沿 ·阅读时间 8 分钟·2023 年 5 月 30 日

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

机器学习(ML)项目在商业中变得越来越受欢迎,因为组织力图通过利用人工智能获得竞争优势或增加市场价值。然而,与传统的软件开发或分析项目不同,机器学习项目的调度可能更加具有挑战性,因为项目的成功往往在完成之前很难预知,且工作流程较为松散。也可以这样说:

“你不知道 ML 项目是否成功,直到模型开发完成并投入生产”

换句话说,你希望在开始一个大型 ML 项目之前,通过有结构的工作流程和合理的期望来确保你的团队能够成功(如果你想了解更多关于 ML 的一般知识,你可以找到最好的教程来自Cassie Kozyrkov)。成功的一个关键因素是有效的沟通,这能够促成合适的工作流程和项目管理。

为什么选择技术图示?

用语言进行沟通是困难的,尤其是当人们讲的语言有所不同(业务方面和技术方面)。此外,描述复杂关系的语言需要大量的文本或长时间的会议。然而,图示的好处在于容易理解,并且可以非常直观(当然前提是制作得当)。有时候,一幅图示(或图片)可以替代 1000 个词。

我和我的团队一开始制作了一个技术图,主要是为了帮助我们自己保持对机器学习项目的总体概览,并确保我们有一个标准化的工作流程。随着时间的推移,我意识到使用这样的图不仅对未来项目有帮助,还可以帮助公司内部的其他人和领导了解一个机器学习项目的内容。到目前为止的整体反馈非常好,所以我想展示我们所做的工作,以及我们如何利用这些图进行沟通、设定截止日期和期望。

技术图:从简单到复杂

复杂的技术图可能令人不知所措,因此最好从简单开始,之后再添加更多层。这是一个机器学习项目的潜在初始图:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

“内循环”的简化图,展示了如何使用特征和目标来迭代训练模型。

如图所示,这个技术图非常高层次,展示了如何通过输入数据生成训练模型所需的两个必要成分:特征和目标。用红色突出显示的是所谓的“内循环”,它描述了使用数据来训练和改进模型的简化迭代过程。

在我们添加更多细节之前,先完成这个过程,并添加模型训练后的情况:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

“外循环”部分的简化图,展示了如何将训练好的模型投入生产并产生输出。

这个图现在添加了生产管道和部分所谓的“外循环”,其中特征按照计划输入模型,最终预测被输出到数据集或直接给用户。

这是我们拥有的高层次但完整的机器学习项目周期技术图。现在是时候添加一些缺失且非常重要的细节了:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

监控对于每一个投入生产的模型都是至关重要的,必须在图中体现。

监控对于每个产品都是必不可少的,尤其是对于机器学习。特征的轻微变化可能会对预测产生巨大影响,反之亦然。如果你想防止客户替你做监控(即等到他们抱怨时才采取行动),建议在模型训练前后密切监控数据。如果你的模型会实时再训练和更新,则需要额外的监控(在图中未显示)。

额外的细节现在非常依赖于图的受众。这个例子的受众一方面是经验丰富的数据科学家组成的机器学习团队,另一方面是需要了解过程的部分技术领导团队。鉴于主要的技术背景,我会首先添加有关特征存储的更多细节:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

良好的特征提取是模型性能的关键,而一个可扩展且可维护的特征提取过程将确保更快的周转。

由于我们处理的是表格数据(随时间变化的地理空间数据),我们不会“仅仅”让数据科学家和数据工程师独自面对抽象的“特征存储”概念。我们将详细定义前期认为对改进模型最有价值的内容,并建立一个能够轻松处理所需数据类型的数据管理系统。例如,我们将不具备时间线的静态上下文与具有时间线但不定期更新的历史动态上下文分开。此外,我们将具有实时数据流的持续动态上下文分开。最终,所有这些不同的数据集汇聚成一个统一的数据集,我们称之为特征存储,以便所有人可以在一个地方拉取和测试新特征,并要求添加更多特征。

对于数据科学家来说,这是一种很好的情况。特征已经可用并以标准方式格式化,因此可以立即开始有趣的工作(训练模型)。但在数据科学家可以开始之前,我们需要调整图中的内部循环:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

内部循环的详细完善,包括添加预处理、将数据集分为 4 个用于建模的部分,以及评估层级。

这张图展示了对内部循环的详细完善,添加了 3 个重要方面到训练周期中:

  • 预处理: 为了使特征存储具有一定的可扩展性,任何对现有特征的处理(例如,为自回归创建滞后版本)将在预处理时通过视图/指针函数即时完成,而不是作为另一种冗余特征添加到存储中。

  • 分成 4 个数据集: 模型的训练是一个迭代过程。然而,为了改进这些迭代,我们需要获取关于如何改进模型的见解,这样会自动引入可能导致过拟合的偏差。因此,我们创建一个用于训练的数据集,一个用于训练验证的数据集,一个用于调试和获得迭代改进见解的数据集,以及一个作为项目最终测试的数据集,以判断模型是否符合成功标准。

  • 评估层级: 考虑到这 4 个数据集,我们还引入了基于验证、调试或测试集的 3 种不同评估。在开始处理内部循环之前,重要的是对这些评估中的“成功”进行对齐并记录下来。只有当测试评估按计划进行时,模型才算成功并准备投入生产。

这样,我们几乎完成了一个中等详细度的机器学习项目技术图纸,只需添加一个小的部分:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

最终的政策层确保过滤掉或替换意外的模型行为。

使用机器学习的一个缺点是模型总会给出结果,无论结果是否合理。因此,无论你的训练数据有多好,由于你无法确保新进入的数据遵循训练数据的规范,你应该预期模型行为有时会出现不愉快的情况。政策层的理念是过滤或替换这些行为。例如,一个预测某地访问人数的模型可能会在某些情况下预测负数。政策层检测到这种情况并将其替换为硬编码的数字或另一个位于其上的模型的输出。

使用颜色或形状进行规划并设置时间表

我们现在已经制定了涵盖整个机器学习项目的技术图纸。然而,即使我们一步一步地进行并逐层建立技术图纸,查看图纸仍然可能令人不知所措。想象一下你需要更多细节。增加的复杂性会使图纸越来越不直观,因此变得无用。

这时直观的颜色编码或形状可以派上用场。例如,我们在图纸中使用颜色编码来跟踪进度。例如,当新的功能被添加到功能存储并且代码在生产中运行时,功能存储任务将标记为绿色。这会通知整个团队(和公司)当前项目的进展情况。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

绿色形状突出显示“完成”的任务。橙色标记“进行中”的任务。所有其他任务则处于“即将开始”状态。

此外,与团队讨论图纸将使设置截止日期变得更容易。可以将图纸拆分成子部分,对于每个子部分,工作所需时间相对可预测。例如,设置功能存储或将现有模型投入生产是可预测的工作任务。然而,内部循环则更难预测,但这里也可以设定估算。例如,通过将内部循环的时间限制为 X 周。到时,测试评估标准必须得到满足,否则,项目将被降级优先级(但可能会在后续被重新考虑)。

总结

从利用机器学习的产品创意到生产就绪状态的过程可能面临独特的挑战。由于其复杂性,机器学习项目难以估算,而且在项目管理薄弱的情况下,只有有限数量的项目能够达到生产状态。技术图纸可以通过提供视觉清晰度和有效沟通来克服这些限制。以直观的方式使项目的整体复杂性可见将有助于团队和整个组织了解项目内容并设定合理的估算。通过这种方式,技术图纸可以促进整个组织更有效的沟通和工作流程。

除非另有说明,所有图片均为作者提供。

总结最佳实践以进行提示工程

原文:towardsdatascience.com/summarising-best-practices-for-prompt-engineering-c5e86c483af4?source=collection_archive---------0-----------------------#2023-05-29

如何使用 OpenAI API 构建自己的基于 LLM 的应用

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Dmytro Nikolaiev (Dimid)

·

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片由Glenn Carstens-Peters拍摄,来源于Unsplash

提示工程指的是为大型语言模型(LLMs),如 OpenAI 的 ChatGPT,创建称为提示的指令的过程。利用 LLMs 解决各种任务的巨大潜力,借助提示工程可以节省大量时间,并促进令人印象深刻的应用程序的开发。它是释放这些大型模型全部能力的关键,改变我们与这些模型的互动方式及其带来的好处。

在这篇文章中,我尝试总结提示工程的最佳实践,以帮助你更快地构建基于 LLM 的应用程序。虽然这个领域发展迅速,但以下这些“经过时间考验”的:) 技术往往效果很好,并能让你取得出色的成果。特别是,我们将涵盖:

  • 迭代提示开发的概念,使用分隔符和结构化输出;

  • 思维链推理;

  • 少样本学习

结合直观的解释,我将分享实际的示例和未来调查的资源。

然后我们将探索如何使用OpenAI API免费构建一个简单的基于 LLM 的本地应用程序。我们将使用 Python 来描述逻辑,使用Streamlit 库来构建网页界面。

让我们开始吧!

提示工程的最佳实践

在这篇文章中,我将通过网页界面和 API 与 ChatGPT 互动。我将使用的gpt-3.5-turbo模型是 ChatGPT 背后的模型,因此你可以直接在浏览器中实验你的提示。

这里一个重要的点是,ChatGPT 不仅仅是大型语言模型(LLM);如你所知,它还是一个 SFT(监督微调)模型,经过了来自人类反馈的强化学习(RLHF)的进一步微调。虽然许多开发者目前利用 OpenAI 的模型进行实验项目和个人探索,但由于隐私和其他原因,其他模型可能更适合在大型企业的生产环境中部署。

如果你想知道为什么基础模型(如GPT-3ChinchillaLLaMA)的功能与微调和 RLHF 训练的助手(如ChatGPTKoalaAlpaca)不同,可以参考Andrej Karpathy 关于训练和使用类似 GPT 模型的讲座。我强烈推荐查看这个讲座以获得更深入的理解,时间只有 40 分钟。总结可以参考这个 Twitter 线程

现在让我们深入探讨针对指令调优 LLMs 的最佳实践!

迭代提示开发

就像任何机器学习模型都是通过迭代过程构建的,有效的提示也是通过类似的迭代方法构建的。即使是最有才华的开发者也可能在第一次尝试时没有创建完美的提示,因此要准备好接受现实,可能需要多次尝试才能实现预期目标。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

基于数据的应用程序构建始终是一个迭代过程。公共领域

通过示例理解事物总是更好。让我们开始构建一个从职位描述中提取信息的系统。我们将在示例中使用的职位描述是来自 LinkedIn 的机器学习工程师招聘广告。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

示例职位描述。来自LinkedIn 职位页面的截图

初始提示可以简单地请求模型提取特定信息。此外,我会使用分隔符(你可以在稍后提到的ChatGPT 提示工程师课程中了解更多)。虽然本地应用程序不太可能受到提示注入攻击的影响,但这仍然是一个好习惯。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

提示 v1 的输出。由作者使用ChatGPT创建的图像

嗯,这样并没有太大帮助。我们需要更具体地说明我们希望从模型中获得什么。让我们要求它提取职位名称、公司名称、所需关键技能以及总结的职位描述。

记住,这只是一个示例,你可以设计你的提示来提取你想要的任何信息:学位、所需经验年限、地点等等。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

提示 v2 的输出。由作者使用ChatGPT创建的图像

看起来更好!为了使输出更简洁明了,让我们要求模型将技能输出为列表,并提供更简短的职位描述总结。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

提示 v3 的输出。由作者使用ChatGPT创建的图像

这是我们初次尝试的显著改进,不是吗?我们仅仅用两次迭代就达到了这一点!所以,当事情最初进展不顺利时,不要失去希望。继续指导模型,进行实验,你一定会取得成功。

请求结构化输出

我想讨论的第二点是要求模型以某种预期的结构化格式输出结果。虽然这对通过网页界面与 LLM 交互(例如我们与 ChatGPT 的方式)可能不是那么关键,但对于基于 LLM 的应用程序来说是极其有用的,因为解析结果的过程要容易得多。

一种常见的做法是使用 JSON 或 XML 等格式,并定义特定的键来组织输出数据。让我们修改提示以展示模型预期的 JSON 结构。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

对于提示 v4 的输出,要求 JSON 输出。由 ChatGPT 创作的作者提供的图像

这样的输出更容易在应用程序的后续逻辑中解析和处理。

值得提及的是这个方向的发展。一些工具旨在严格将模型的输出适配到给定的格式,这对某些任务非常有用。只需看看下面的示例!

其中一个可能的应用是生成特定格式的大量内容(例如,使用 guidance 的游戏角色信息)。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

生成 JSON 格式的游戏角色信息。来自 guidance GitHub 仓库 的 gif

LMQL 这样的语言为提示语言模型引入了类似编程的方法。随着这些工具的不断发展和改进,它们有可能彻底改变我们与 LLM 的互动方式,从而提供更准确和结构化的响应。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

LMQL 查询示例。请参阅 LMQL 网页,更多示例请见此处 的截图

Chain-of-Thought 推理

Chain-of-Thought (CoT) 推理被发现对于需要……好吧,推理的任务非常有帮助。因此,如果你有机会通过将任务分解成多个更简单的步骤来解决问题,这对于 LLM 来说可能是一个很好的方法。

看看原始论文中的示例。通过将问题拆分成更小的步骤并提供明确的指示,我们可以帮助模型生成正确的输出。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

介绍 CoT 提示。来自 Chain-of-Thought Prompting Elicits Reasoning in LLMs 论文 的图 1

有趣的是,后来发现,在提示的末尾附加一个简单的且神奇的 “让我们一步一步思考” 可以改善结果——这种技巧被称为 零-shot CoT。因此,构建允许模型“思考过程”的提示是很有帮助的,因为模型没有其他表达思想的能力,除了生成标记。

目前最佳的零-shot CoT 提示是“让我们一步一步地解决这个问题,以确保我们得到正确的答案”。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

最佳零样本提示。来自LLMs 是人类水平的提示工程师论文的表 7

更复杂的解决方案正在积极开发中。虽然它们在某些场景中显著超越其他方法,但其实际应用仍然有限。我将提到两种这样的技术:自一致性思维树

自一致性论文的作者提出了以下方法。他们建议不要仅仅依赖于初始模型输出,而是通过多次采样并通过多数投票来聚合结果。通过依赖直觉和集成方法在经典机器学习中的成功,这种技术增强了模型的鲁棒性。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

自一致性。来自自一致性改进语言模型中的链式推理论文的图 1

你也可以在不实施聚合步骤的情况下应用自一致性。对于短输出的任务,要求模型建议几个选项并选择最佳选项。

思维树(ToT)将这一概念进一步扩展。它提出了为模型的“推理思维”应用树搜索算法的想法,当模型遇到不良假设时,实际上是回溯。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

思维树。来自思维树:使用 LLMs 进行深思熟虑问题解决论文的图 1

如果你感兴趣,可以查看Yannic Kilcher 的 ToT 论文评论视频

对于我们的具体场景,利用链式思维推理并非必要,但我们可以将模型的总结任务分为两个阶段。最初,它可以概括整个职位描述,然后再对得出的总结进行集中于职位职责的总结。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

带有逐步指令的提示 v5 的输出。由ChatGPT创建的作者图像

在这个特定的例子中,结果没有显示出显著的变化,但这种方法对大多数任务非常有效。

少样本学习

我们将介绍的最后一种技术叫做少样本学习,也称为上下文学习。它的简单之处在于将几个示例纳入提示中,以便为模型提供更清晰的任务描述。

这些示例不仅要与任务相关,还要多样化,以涵盖数据的多样性。对于少量样本学习,使用 CoT 可能会有些挑战,特别是当你的流程有很多步骤或输入较长时。然而,通常来说,结果是值得这些努力的。另外,请记住,标记少量示例的成本远低于在传统机器学习模型开发中标记整个训练/测试集的成本。

如果我们在提示中添加一个示例,它将更好地理解要求。例如,如果我们表明我们希望最终总结以要点形式呈现,模型将会按照我们的模板进行回应。

这个提示可能会让人感到有些压倒,但不要害怕:它只是一个以前的提示(v5)和一个标记的示例,采用 For example: 'input description' -> 'output JSON' 格式。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

包含一个示例的提示 v6 输出。图像由作者使用ChatGPT创建

总结最佳实践

总结提示工程的最佳实践,请考虑以下几点:

  • 不要害怕实验。尝试不同的方法,逐步迭代,纠正模型并一次进行小的步骤;

  • 在输入中使用分隔符(例如 <>),并要求结构化输出(例如 JSON);

  • 提供完成任务的行动列表。尽可能地,向模型提供一组行动,并让它输出“内部想法”;

  • 如果输出较短,请求多个建议

  • 提供示例。如果可能,向模型展示几个代表数据的多样化示例,并显示期望的输出。

我认为这个框架为自动化各种日常任务提供了足够的基础,例如信息提取、总结、文本生成(如电子邮件)等。然而,在生产环境中,仍然可以通过在特定数据集上微调模型来进一步优化性能。此外,插件代理的快速发展也值得关注,但那是完全不同的话题。

DeepLearning.AI 和 OpenAI 的提示工程课程

除了之前提到的Andrej Karpathy 的讲座,这篇博客文章还从DeepLearning.AI 和 OpenAI 的 ChatGPT 提示工程课程中汲取了灵感。该课程完全免费,完成仅需几小时,并且我个人非常喜欢的一点是,它允许你在无需注册的情况下实验 OpenAI API!

这是一个很好的实验场地,所以一定要去看看。

使用 OpenAI API 和 Streamlit 构建基于 LLM 的应用程序

哇,我们涵盖了很多信息!现在,让我们继续前进,开始使用我们获得的知识构建应用程序吧。

生成 OpenAI 密钥

要开始使用,你需要注册一个 OpenAI 账户并创建你的 API 密钥。OpenAI 目前为每个人提供3 个月的 $5 免费信用额。请参考OpenAI API 入门介绍页面来注册你的账户和生成你的 API 密钥

一旦你有了密钥,创建一个OPENAI_API_KEY 环境变量以便通过os.getenv('OPENAI_API_KEY')在代码中访问它。

使用标记器实验室估算成本

在这个阶段,你可能会好奇仅凭免费试用可以做多少事情,以及初始三个月后有哪些选项。这是个很好的问题,特别是当你考虑到LLMs 需要花费数百万美元

当然,这些数百万是关于训练的。事实证明,推理请求非常实惠。虽然 GPT-4 可能被认为比较贵(尽管价格可能会下降),但gpt-3.5-turbo(默认 ChatGPT 背后的模型)仍然足以应对大多数任务。实际上,考虑到这些模型的原始参数量以亿计,OpenAI 做了一个令人难以置信的工程工作,因为这些模型现在既便宜又快速。

gpt-3.5-turbo 模型的费用是每 1,000 个标记 $0.002。

那么,具体多少钱呢?让我们看看。首先,我们需要了解什么是标记。简单来说,标记指的是单词的一部分。在英语中,你可以预期每 10 个单词大约有 14 个标记

为了更准确地估算你特定任务和提示的标记数量,最好的方法是亲自尝试!幸运的是,OpenAI 提供了一个标记器实验室来帮助你。

附注:不同语言的标记化

由于英语在互联网的广泛使用,这种语言的标记化效果最好。正如在“所有语言的标记化并不相等”博客文章中强调的那样,标记化在不同语言中并不统一,某些语言可能需要更多的标记来表示。如果你想构建一个涉及多语言提示的应用程序,比如翻译,请记住这一点。

为了说明这一点,我们来看看不同语言中 全句 的分词情况。在这个玩具示例中,英语需要 9 个标记,法语 — 12,保加利亚语 — 59,日语 — 72,俄语 — 73。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

不同语言的分词。截图来自 OpenAI 分词器游乐场

成本与性能

正如你可能注意到的那样,提示可能会变得相当长,尤其是在包含示例时。通过增加提示的长度,我们可能会提高质量,但同时随着使用更多标记,成本也会增加。

我们最新的提示(v6)大约由 1.5k 个标记组成。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

提示的分词 v6。截图来自 OpenAI 分词器游乐场

考虑到输出长度通常与输入长度相同,我们可以估计每次请求的平均标记数约为 3k 个(输入标记 + 输出标记)。通过将这个数字乘以初始费用,我们发现每次请求约为 0.006 美元或 0.6 美分,这相当实惠。

即使我们考虑到每次请求稍高的费用为 1 美分(相当于大约 5k 个标记),你仍然可以以仅需 1 美元进行 100 次请求。此外,OpenAI 提供了设置软限制和硬限制的灵活性。软限制会在你接近定义的限制时发出通知,而硬限制则会限制你超出指定阈值。

对于本地使用的 LLM 应用程序,你可以舒适地配置每月 1 美元的硬限制,确保在预算范围内享受模型的好处。

Streamlit 应用模板

现在,让我们构建一个网页界面,以编程方式与模型互动,消除每次手动复制提示的需要。我们将使用 Streamlit 来完成这一任务。

Streamlit 是一个 Python 库,它允许你创建简单的网页界面,无需使用 HTML、CSS 和 JavaScript。它对初学者友好,并允许使用最少的 Python 知识创建基于浏览器的应用程序。现在我们来创建一个简单的模板,用于基于 LLM 的应用程序。

首先,我们需要处理与 OpenAI API 通信的逻辑。在下面的示例中,我假设 generate_prompt() 函数已定义并返回给定输入文本的提示(例如,类似于你之前看到的)。

就这样!了解更多关于不同参数的信息,请参考 OpenAI 的文档,但默认设置已经很好。

有了这些代码,我们可以设计一个简单的网页应用程序。我们需要一个输入文本的字段,一个处理文本的按钮,以及几个输出小部件。我更倾向于访问完整的模型提示和输出,以便进行调试和探索原因。

整个应用程序的代码大致如下,可以在这个 GitHub 仓库中找到。由于共享 OpenAI 密钥不是一个好主意,我添加了一个名为toy_ask_chatgpt()的占位符函数。目前,这个应用程序只是将提示复制到输出中。

如果不定义函数和占位符,这只有大约 50 行代码!

幸好Streamlit 最近的更新现在允许嵌入它到这篇文章中!所以你应该能够在下方看到它。

现在你可以看到这有多么简单。如果你愿意,你可以使用 Streamlit Cloud 部署你的应用程序。但要小心,因为如果你在其中放置你的 API 密钥,每个请求都会花费你金钱!

结论

在这篇博客文章中,我列出了几种提示工程的最佳实践。我们讨论了迭代提示开发、使用分隔符、请求结构化输出、思维链推理以及少量示例学习。我还提供了一个模板,用于在不到 100 行代码的情况下使用 Streamlit 构建一个简单的网页应用程序。现在,轮到你来提出一个令人兴奋的项目创意并将其变为现实了!

现代工具允许我们在仅仅几小时内创建复杂的应用程序,这真是令人惊叹。即使没有丰富的编程知识、Python 熟练度或对机器学习的深刻理解,你也可以快速构建一些有用的东西并自动化一些任务。

如果你是初学者并且想创建类似的项目,请随时向我提问。我将非常乐意协助你,并尽快回复。祝你的项目好运!

资源

这里是我关于 LLM 的其他文章,希望对你有所帮助。我已经涵盖了:

  • 估算大语言模型的规模:LLM 是什么,如何训练,它们需要多少数据和计算资源;

  • 使用 ChatGPT 进行调试:如何使用 LLM 进行调试和代码生成。

你可能还会对以下内容感兴趣:

感谢阅读!

  • 希望这些材料对你有所帮助。在 Medium 上关注我,以获取更多类似的文章。

  • 如果你有任何问题或评论,我会很高兴收到任何反馈。可以在评论中问我,或通过LinkedInTwitter联系我。

  • 支持我作为作者并访问其他成千上万篇 Medium 文章,请通过我的推荐链接获取 Medium 会员(对你没有额外费用)。

使用 NLP 和 AI 更好地总结播客文字记录和长文本

原文:towardsdatascience.com/summarize-podcast-transcripts-and-long-texts-better-with-nlp-and-ai-e04c89d3b2cb?source=collection_archive---------0-----------------------#2023-05-03

为什么现有的总结方法存在缺陷,以及如何改进的详细步骤

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Isaac Tham

·

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片来源于 Unsplash。

像 GPT-4 这样的 LLM 已经席卷了世界,而生成文本模型特别擅长于长文本的摘要,如书籍或播客转录。然而,使用 LLM 来总结长文本的传统方法实际上存在根本性的缺陷。在这篇文章中,我将告诉你现有摘要方法的问题,并提出一种更好的摘要方法,实际上考虑了文本的结构!更棒的是,这种方法还会给我们文本的主要话题——一举两得!

我将引导你如何通过对现有方法进行几个小调整,在 Python 中轻松实现这一点。这是我们在Podsmart中使用的方法,我们新推出的 AI 驱动的播客摘要应用,帮助忙碌的知识分子节省听取的时间。

现有解决方案的问题

总结长文本的经典方法是递归摘要,其中长文本被平分成较短的块,这些块可以适应 LLM 的上下文窗口。每个块都被总结,摘要被连接在一起,然后传递给 GPT-3 进行进一步总结。这个过程会重复进行,直到获得所需长度的最终摘要。

然而,主要的缺点是现有实现,例如 LangChain 的summarize chain using map_reduce,将文本分割成块,而不考虑文本的逻辑和结构流。

例如,如果文章长 1000 字,200 字的块大小意味着我们将获得 5 个块。如果作者有几个主要点,其中第一个占据了前 250 字?最后 50 字会被放入第二个块中,与作者的下一个观点的文本一起处理,传递这个块给 GPT-3 的摘要器可能会导致遗漏第一个观点的潜在重要信息。此外,一些关键点可能比其他点更长,并且事先无法知道这一点。

另一种方法是“精炼”方法,它将每一段文本以及来自前几段的摘要传递给 LLM,这样随着看到更多的文本,摘要会逐步被精炼(请参见这里的提示)。然而,该过程的顺序特性意味着它无法并行处理,且所需时间是线性的,远长于递归方法的对数时间。此外,直觉上初始部分的意义在最终摘要中会被过度代表。对于播客转录,其中前几分钟是与播客其余部分完全无关的广告,这成为一个绊脚石。因此,这种方法并不被广泛使用。

即使出现了更先进的语言模型,具有更长的上下文窗口,它仍然会在许多总结用例(整个书籍)中显得极其不足,因此不可避免地需要一些分块和递归总结。

本质上,如果总结过程未能识别文本的意义层次结构,并且与之不兼容,那么生成的总结很可能不足以准确传达作者的意图。

更好的前进方式

更好的解决方案是将总结和主题建模过程在同一个算法中一起处理。在这里,我们将递归总结的一个步骤的总结输出拆分为块,然后将这些块输入到下一步。我们可以通过将块在语义上进行主题聚类,并将主题传递到下一次总结迭代中来实现这一点。让我们来看看如何在 Python 中实现这一点吧!

要求

Python 包:

  • scipy — 用于余弦距离度量

  • networkx — 用于 Louvain 社区检测算法

  • langchain — 一个实用功能包,允许你调用像 OpenAI 的 GPT-3 这样的 LLMs。

数据和预处理

包含 Jupyter notebook 和数据的 GitHub 仓库可以在这里找到:github.com/thamsuppp/llm_summary_medium

我们今天总结的文本是 2023 年美国总统乔·拜登的国情咨文演讲。文本文件在 GitHub 仓库中,这里 是原始来源。演讲,像所有美国政府出版物一样,属于公有领域。请注意,确保你被允许使用源文本很重要——《Towards Data Science》发布了一些有用的提示,关于如何检查数据集的版权和许可证。

我们将原始文本拆分为句子,限制句子的最小长度为 20 个单词,最大长度为 80 个单词。

创建块

与其创建足够大以适应上下文窗口的块,我建议块的大小应为表达一个离散思想通常需要的句子数量。这是因为我们随后将嵌入这个文本块,本质上将其语义意义提炼成一个向量。我目前使用 5 个句子(但你可以尝试其他数量)。我倾向于在块之间有 1 句重叠,以确保连续性,使每个块都能包含一些关于前一个块的上下文信息。对于给定的文本文件,共有 65 个块,平均块长 148 个单词,范围从 46 到 197 个单词。

获取每个块的标题和摘要

现在,这是我开始偏离 LangChain 的总结链的地方。

用 1 次 LLM 调用获得 2 个:标题 + 摘要

我想要既获得一个信息丰富的标题,又要对每个块进行总结(标题的重要性会在后面变得更加明确)。因此,我创建了一个自定义提示,改编了 Langchain 的 默认总结链提示。正如你在 map_prompt_template 中看到的 - text 是一个将被插入到提示中的参数 - 这将是每个块的原始文本。我创建了一个 LLM,目前是 GPT-3,并创建了一个 LLMChain,将 LLM 与提示模板结合起来。然后,map_llm_chain.apply() 调用 GPT-3,并将插入文本的提示模板传入,返回每个块的标题和摘要,我将这些解析为字典输出。注意,所有块可以并行处理,因为它们彼此独立,从而带来了巨大的速度优势。

你可以使用 ChatGPT 以更便宜的价格和类似的性能,但我尝试过的时候,只有 GPT-3 LLM 能并行运行查询,而使用 ChatGPT 则是逐个运行,这非常缓慢,因为我通常会同时传入 ~100 个块。并行运行 ChatGPT 需要异步实现。

def summarize_stage_1(chunks_text):

  # Prompt to get title and summary for each chunk
  map_prompt_template = """Firstly, give the following text an informative title. Then, on a new line, write a 75-100 word summary of the following text:
  {text}
  Return your answer in the following format:
  Title | Summary...
  e.g. 
  Why Artificial Intelligence is Good | AI can make humans more productive by automating many repetitive processes.
  TITLE AND CONCISE SUMMARY:"""
  map_prompt = PromptTemplate(template=map_prompt_template, input_variables=["text"])
  # Define the LLMs
  map_llm = OpenAI(temperature=0, model_name = 'text-davinci-003')
  map_llm_chain = LLMChain(llm = map_llm, prompt = map_prompt)
  map_llm_chain_input = [{'text': t} for t in chunks_text]
  # Run the input through the LLM chain (works in parallel)
  map_llm_chain_results = map_llm_chain.apply(map_llm_chain_input)
  stage_1_outputs = parse_title_summary_results([e['text'] for e in map_llm_chain_results])
  return {
    'stage_1_outputs': stage_1_outputs
  }

嵌入块并按主题进行聚类

在获得每个块的摘要后,我将使用 OpenAI 的嵌入将它们嵌入到 1536 维向量中。传统的递归总结方法不需要嵌入,因为它们按均匀长度任意拆分文本。对我们来说,我们的目标是通过将语义上相似的块分组到一起,来改进这一方法。

将文本按主题分组是 NLP 中一个研究较多的问题,许多传统方法如 Latent Dirichlet Allocation 早于深度学习时代。我记得在 2017 年使用 LDA 来为我大学报纸的文章进行聚类——它估计的速度非常慢,并且只使用词频,这不能捕捉语义含义。

现在,我们可以利用 OpenAI 的嵌入即服务 API 来获取在一秒钟内捕捉句子语义含义的嵌入。这里还有许多其他可能的嵌入模型,例如 HuggingFace 的 sentence-transformers,据报道它比 OpenAI 的嵌入表现更好,但这涉及到下载模型并在你自己的服务器上运行。

在获得块的嵌入向量后,我们将相似的向量聚集在一起。

我创建了一个块相似性矩阵,其中 (i,j)th 条目表示第 i 个和第 j 个块的嵌入向量之间的余弦相似性,即块之间的语义相似性。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

国情咨文演讲的片段相似度矩阵。你可以看到某些片段组在语义上彼此相似——这正是话题检测算法随后会揭示的内容。图片由作者提供。

我们可以将其视为节点之间的相似度图,其中节点是片段,边的权重是两个片段之间的相似度。我们使用 Louvain 社区检测算法 从片段中检测话题。这是因为在图分析中,社区被定义为具有密集的内部社区连接和稀疏的社区间连接,这正是我们想要的:话题内的片段彼此非常语义相似,而每个片段与其他检测到的话题中的片段的语义相似度较低。

Louvain 社区检测算法有一个超参数叫做分辨率——较小的分辨率会导致较小的簇。此外,我增加了一个超参数 proximity_bonus —— 如果原始文本中片段的位置彼此接近,它会提高片段的相似度分数。你可以将此解释为将文本的时间结构视为先验(即,彼此接近的片段更可能在语义上相似)。我加入这个参数是为了避免检测到的话题中包含来自文本各处的片段,这种情况不太可能。该函数还试图最小化簇大小的方差,防止出现一个簇有 1 个片段而另一个簇有 13 个片段的情况。

对于国情咨文演讲,输出是 10 个簇,这些簇之间的连贯性很好。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

检测到的国情咨文演讲的话题簇。图片由作者提供。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

检测到的 Bloomberg Surveillance 播客转录的话题簇。紫色和橙色的话题识别了广告。图片由作者提供。

第二张图片是另一个播客节目中的话题聚类。正如你所见,开始和结束部分被检测为相同的话题,这在开头和结尾有广告的播客中很常见。一些话题,比如紫色的,也是不连续的——我们的算法允许这种情况很不错,因为文本可能回到之前提到的话题,而这也是传统文本拆分未考虑的另一种可能性。

话题标题和总结

现在,我们得到的是语义一致的话题,可以进入递归总结的下一步。对于这个例子,这将是最后一步,但对于更长的文本如书籍,你可以想象重复这个过程几次,直到剩下大约 10 个话题,其话题总结可以适应上下文窗口。

下一步涉及三个不同的部分。

主题标题:对于每个主题,我们生成了该主题中块的标题列表。我们将所有主题的标题列表传递给 GPT-3,并要求其聚合这些标题以得出每个主题的一个标题。我们对所有主题同时进行此操作,以防主题标题之间过于相似。以前,当我单独生成主题标题时,GPT-3 没有其他主题标题的上下文,因此出现了 7 个标题中有 4 个是‘联邦储备的货币政策’的情况。这就是我们希望生成块标题的原因——试图将所有块摘要放入上下文窗口在非常长的文本中可能不可行。

如下所示,标题看起来很好!描述性强,但彼此独特。

1\. Celebrating American Progress and Resilience
2\. US Economy Strengthening and Inflation Reduction
3\. Inflation Reduction Act: Lowering Health Care Costs
4\. Confronting an Existential Threat: Making Big Corporations Pay
5\. Junk Fee Prevention Act: Stopping Unfair Charges
6\. COVID-19 Resilience and Vigilance
7\. Fighting Fraud and Public Safety
8\. United States' Support for Ukraine and Global Peace
9\. Progress Made in Healthcare and Gun Safety
10\. United States of America: A Bright Future

主题摘要: 没有新意,这涉及将每个主题的块摘要结合在一起,并要求 GPT-3 将其总结为主题摘要。

最终摘要: 为了得到文本的总体摘要,我们再次将主题摘要连接在一起,并提示 GPT-3 进行总结。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

国情咨文的最终摘要。图像由作者提供。

总结

我们的方法有哪些好处?

文本被分层拆分为主题、块和句子。随着层级的深入,我们得到越来越详细和具体的摘要,从最终摘要,到每个主题的摘要,再到每个块的摘要。

正如我上面提到的,摘要准确地捕捉了文本的语义结构——其中有一个总体主题,分成几个主要主题,每个主题包含若干关键思想(块),确保在各层摘要中保留了关键信息。

这比仅仅提供总体摘要更具灵活性。不同的人对文本的不同部分更感兴趣,因此他们会选择适合自己需求的详细程度。

当然,这需要将生成的摘要与直观且连贯的界面配对,该界面可视化文本的层级特性。此类可视化的一个示例见 Podsmart—— 点击这里 查看演讲的互动摘要。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

提取的主题及其时间线的可视化。图像由作者提供。

请注意,这并不会显著增加 LLM 的费用——我们仍然将与传统方法相同的输入传递给 LLM,但我们获得了更丰富的摘要。

TLDR —— 这里是生成优质文本摘要的秘密法宝

  1. 语义一致的主题——通过对文本的小块进行语义嵌入并按语义相似性拆分文本

  2. 从数据块中获取标题和摘要——这需要自定义提示,而不是使用默认的 LangChain 摘要链

  3. 校准 Louvain 社区检测算法——如分辨率和接近度奖励等超参数确保生成的话题簇是合理的

  4. 不同的话题标题——同时生成所有话题标题,这需要块标题

再次提醒,你可以在 GitHub 仓库 查看整个源代码

如果你觉得这篇文章对你有帮助:

  1. 请查看我在 Medium 上的其他文章:作为数据科学家构建 AI 应用的技术提示、使用深度学习生成音乐

  2. 试试我的 app——Podsmart 可以转录和总结播客和 YouTube 视频,为忙碌的知识分子节省听音时间

  3. 关注我在 LinkedInTwitter/X,通过消息或评论与我联系!我很乐意讨论关于数据科学和 AI 的各种想法

感谢阅读!

用 ChatGPT 总结最新的 Spotify 发布内容

原文:towardsdatascience.com/summarizing-the-latest-spotify-releases-with-chatgpt-553245a6df88

探索音乐发现的力量:使用 ChatGPT 或 GPT-4 和 Spotify API 总结新发布的音乐

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Luís Roque

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

在当今快节奏的世界中,自然语言处理(NLP)已成为各种应用中至关重要的组成部分。像 OpenAI 的 ChatGPT 和 GPT-4 这样的巨大模型,解锁了在摘要生成、语音转文字、语音识别、语义搜索、问答系统、聊天机器人等任务中令人难以置信的潜力。

我很高兴地宣布“大型语言模型编年史:驾驭 NLP 前沿”这一新的每周文章系列,将探讨如何利用大型模型的力量进行各种 NLP 任务。通过深入研究这些前沿技术,我们旨在赋能开发者、研究人员和爱好者,充分利用 NLP 的潜力,解锁新的可能性。

在本系列的第一篇文章中,我们将重点介绍如何使用 OpenAI 的 ChatGPT 和 Spotify API 创建一个智能摘要系统,以获取最新的音乐发布。随着系列的发展,我们将深入探讨多种 NLP 应用,提供洞见、技术和实际示例,展示大型模型在改变我们与语言互动和理解方式方面的能力。

敬请关注更多文章,随着我们踏上这段激动人心的 NLP 之旅,指导你掌握各种语言任务的最新大型模型。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 1:LLM 是否开始了人与机器之间的新合作? (source)

代码可在我的Github上找到。

介绍

ChatGPT 和 GPT-4,由 OpenAI 开发,是先进的语言模型,在各种自然语言处理任务中表现出色。它们能够理解上下文,生成类似人类的响应,甚至有效总结大段文本。这使它们成为总结 Spotify 上最新音乐发布的理想工具。

作为领先的音乐流媒体平台,Spotify 提供了一个广泛的 API,使开发者能够访问大量音乐数据,包括新发布、播放列表等。通过将 ChatGPT 强大的语言理解能力与 Spotify API 提供的丰富音乐数据结合起来,我们可以构建一个系统,让您及时了解 Spotify 目录中的最新内容。

我们将引导您完成构建这个智能音乐总结系统的过程。我们的方法将包括以下步骤:

  1. 访问 Spotify API:我们将通过 Spotify API 获取最新音乐发布的数据。

  2. 使用 ChatGPT 总结:然后,我们将使用 OpenAI 的 API 生成最新发布内容的简明总结。

  3. 结果:最后,我们将以一种易于阅读和引人入胜的格式展示总结。

敬请关注,我们将深入探讨每一步的细节,助您创建自己的音乐总结工具!

访问 Spotify API

在本节中,我们将探讨如何从 Spotify API 获取最新的音乐发布及其相关曲目数据。然后我们将把这些数据保存到 JSON 文件中以供进一步处理。将使用以下 Python 函数实现此目标:

  1. get_new_releases:从 Spotify 获取新专辑发布。

  2. get_album_tracks:检索特定专辑的曲目信息。

  3. save_data_to_file:将获取的数据保存到 JSON 文件中。

  4. load_data_from_file:从 JSON 文件中加载保存的数据。

  5. download_latest_albums_data:从 Spotify 下载最新的专辑和曲目数据,并保存到 JSON 文件中。

让我们解析这些功能的关键组件,理解它们如何协同工作以访问 Spotify API。

获取新发布

get_new_releases 函数接受两个可选参数,limitoffsetlimit 确定要返回的专辑结果的最大数量,而 offset 指定第一个结果的索引。默认情况下,limit 设置为 50,offset 设置为 0。然后,函数调用 Spotify API 的 sp.new_releases,它返回一个包含专辑信息的字典。相关的专辑项被提取并返回为字典列表。

def get_new_releases(limit: int = 50, offset: int = 0) -> List[Dict[str, Any]]:
    """
    Fetch new releases from Spotify.

    Args:
        limit (int, optional): Maximum number of album results to return. Defaults to 50.
        offset (int, optional): The index of the first result to return. Defaults to 0.

    Returns:
        List[Dict[str, Any]]: A list of dictionaries containing album information.
    """
    new_releases = sp.new_releases(limit=limit, offset=offset)
    albums = new_releases["albums"]["items"]
    return albums

检索专辑曲目

get_album_tracks 函数接受一个参数 album_id,这是我们想要获取曲目信息的专辑的 Spotify ID。该函数调用 Spotify API 的 sp.album_tracks,返回一个包含曲目数据的字典。然后,曲目项被提取并作为字典列表返回。

def get_album_tracks(album_id: str) -> List[Dict[str, Any]]:
    """
    Fetch tracks from a specific album.

    Args:
        album_id (str): The Spotify ID of the album.

    Returns:
        List[Dict[str, Any]]: A list of dictionaries containing track information.
    """
    tracks = sp.album_tracks(album_id)["items"]
    return tracks

保存和加载数据

save_data_to_file函数接受两个参数:data,这是一个包含专辑和曲目信息的字典列表;file_path,这是保存数据的 JSON 文件路径。该函数使用json.dump方法将数据写入指定的文件。

相反,load_data_from_file函数从指定的 JSON 文件中读取数据,并使用json.load方法将其作为字典列表返回。

def save_data_to_file(data: List[Dict[str, Any]], file_path: str) -> None:
    """
    Save data to a JSON file.

    Args:
        data (List[Dict[str, Any]]): List of dictionaries containing album and track information.
        file_path (str): Path to the JSON file where the data will be saved.
    """
    with open(file_path, "w", encoding="utf-8") as file:
        json.dump(data, file, ensure_ascii=False, indent=4)

def load_data_from_file(file_path: str) -> List[Dict[str, Any]]:
    """
    Load data from a JSON file.

    Args:
        file_path (str): Path to the JSON file where the data is stored.

    Returns:
        List[Dict[str, Any]]: List of dictionaries containing album and track information.
    """
    with open(file_path, "r", encoding="utf-8") as file:
        return json.load(file)

下载最新专辑数据

download_latest_albums_data函数作为从 Spotify 下载最新专辑和曲目数据的主要驱动程序。它初始化了诸如limitoffsettotal_albumsalbum_count等变量,并创建了一个空列表all_albums以存储获取的数据。

函数进入一个循环,直到获取到指定数量的专辑(total_albums)为止。在每次迭代中,函数调用get_new_releasesget_album_tracks以检索专辑和曲目信息。这些数据随后存储在all_albums列表中。

在获取数据后,函数将offsetlimit值递增,以便在随后的迭代中获取下一组专辑。为了避免触及 Spotify API 的速率限制,添加了 1 秒的延迟。函数最后调用save_data_to_file将获取的数据存储到 JSON 文件中。

def download_latest_albums_data() -> None:
    """
    Download the latest albums and tracks data from Spotify and save it to a JSON file.
    """
    limit = 50
    offset = 0
    total_albums = 30
    album_count = 0

    all_albums = []

    while total_albums is None or album_count < total_albums:
        new_releases = get_new_releases(limit, offset)
        if total_albums is None:
            total_albums = sp.new_releases()["albums"]["total"]

        for album in new_releases:
            album_info = {
                "album_name": album["name"],
                "artist_name": album["artists"][0]["name"],
                "album_type": album["album_type"],
                "release_date": album["release_date"],
                "available_markets": album["available_markets"],
                "tracks": [],
            }

            tracks = get_album_tracks(album["id"])

            for track in tracks:
                track_info = {
                    "track_name": track["name"],
                    "duration_ms": track["duration_ms"],
                    "preview_url": track["preview_url"],
                }
                album_info["tracks"].append(track_info)

            all_albums.append(album_info)
            album_count += 1

        offset += limit
        time.sleep(1)  # Add a delay to avoid hitting the rate limit
        print(f"Downloaded {album_count}/{total_albums}")

    save_data_to_file(all_albums, "albums_and_tracks.json")

通过使用这些函数,我们可以有效地访问 Spotify API,以收集最新音乐发行的数据。在下一节中,我们将探讨如何预处理这些数据并使用 ChatGPT 生成这些新发行的摘要。

使用 LangChain 通过 ChatGPT 生成摘要

在本节中,我们将讨论如何预处理从 Spotify API 获得的专辑和曲目数据,并利用 ChatGPT 通过 LangChain 库生成最新音乐发行的摘要。LangChain 是一个强大的工具,使开发者能够构建将 LLMs 与其他计算或知识源结合的应用程序。

我们将使用以下 Python 函数来实现这一目标:

  1. preprocess_docs: 将 JSON 数据转换为 Document 对象列表。

  2. get_summary: 使用提供的 JSON 数据生成摘要,该数据在 Document 对象的列表中。

数据预处理

preprocess_docs函数接受一个包含专辑和曲目信息的字典列表,这是我们从 Spotify API 检索到的数据。该函数将这些数据转换为 JSON 字符串,然后将其拆分为 3500 字符的段落。这些段落用于创建 Document 对象列表,并将传递给 ChatGPT 以生成摘要。

将数据拆分为较小的段落是为了处理 ChatGPT API 施加的文本长度限制。通过将文本拆分成较小的部分,我们可以更有效地处理数据,而不会超出模型的最大令牌限制。

def preprocess_docs(data: List[Dict[str, Any]]) -> List[Document]:
    """
    Convert the JSON data to a list of Document objects.

    Args:
        data (List[Dict[str, Any]]): List of dictionaries containing album and track information.

    Returns:
        List[Document]: A list of Document objects containing the JSON data as strings, split into 3000-character segments.
    """
    json_string = json.dumps(data, ensure_ascii=False, indent=4)
    doc_splits = [json_string[i : i + 3500] for i in range(0, len(json_string), 3500)]
    docs = [Document(page_content=split_text) for split_text in doc_splits]
    return docs

使用 LangChain 通过 ChatGPT 生成摘要

LangChain 的 CombineDocuments 链旨在处理和组合多个文档的信息,使其非常适合诸如摘要和问答等任务。在我们的案例中,我们将专注于使用 MapReduce 方法生成最新 Spotify 发布内容的摘要。如果你已经可以访问 API,你可以轻松地使用 GPT-4。为此,你只需更新传递给ChatOpenAI类的model_name参数。

MapReduce 方法通过对每个数据块运行初始提示来工作,为每个数据块生成一个输出。例如,在摘要任务中,这涉及为每个单独的数据块创建一个摘要。在下一步中,会运行不同的提示,将所有这些初始输出合并成一个连贯的输出。

使用 MapReduce 方法的主要优势在于它可以扩展到更大的文档并处理比 Stuffing 方法更多的文档。此外,对每个文档的 LLM 调用是独立的,允许并行处理和更快的处理速度。

在我们的项目背景下,我们将应用 MapReduce 方法使用 ChatGPT 总结最新的 Spotify 发布内容。我们使用 MapReduce 方法为每个文档生成摘要,然后将这些摘要合并成一个简洁的总结。

def get_summary(docs: List[Document]) -> str:
    """
    Generate a summary using the JSON data provided in the list of Document objects.

    Args:
        docs (List[Document]): A list of Document objects containing the JSON data as strings.

    Returns:
        str: The generated summary.
    """
    llm = ChatOpenAI(temperature=0, model_name="gpt-3.5-turbo")

    prompt_template = """Write a short summary about the latest songs in Spotify based on the JSON data below: \n\n{text}."""
    prompt_template2 = """Write an article about the latest music released in Spotify (below) and adress the change in music trends using the style of Rick Beato. : \n\n{text}"""

    PROMPT = PromptTemplate(template=prompt_template, input_variables=["text"])
    PROMPT2 = PromptTemplate(template=prompt_template2, input_variables=["text"])

    chain = load_summarize_chain(
        llm,
        chain_type="map_reduce",
        return_intermediate_steps=True,
        map_prompt=PROMPT,
        combine_prompt=PROMPT2,
        verbose=True,
    )

    res = chain({"input_documents": docs}, return_only_outputs=True)

    return res

结果

为了更好地理解我们使用 Spotify API 和 OpenAI API 的 ChatGPT 实现的摘要能力,我们将展示一个示例,演示系统如何处理数据并生成简洁的摘要。让我们检查输入数据、中间步骤和最终输出。

输入数据

输入数据包括几个专辑及其相应的曲目,如 Dillaz 的“Oitavo Céu”和 T-Rex 的“CASTANHO”。每张专辑包括专辑名称、艺术家名称、专辑类型、发行日期以及曲目名称和持续时间(以毫秒为单位)的列表。

中间步骤

中间步骤包括使用 MapReduce 方法处理输入数据。例如,以下是为部分输入数据生成的摘要:

Spotify 上的最新歌曲包括三张新专辑中的曲目:Dillaz 的“Oitavo Céu”、T-Rex 的“CASTANHO”和 Branko 的“OBG”。“Oitavo Céu”包含 12 首曲目,其中包括标题曲目和持续时间最长的“Maçã”,其持续时间为 219130 ms。“CASTANHO”有 11 首曲目,其中“LADO NENHUM”的持续时间最长,为 278190 ms。“OBG”有 10 首曲目,其中“ETA”的持续时间最长,为 226058 ms。所有三张专辑均于 2022 年 4 月发布。

最终输出

最终输出结合了中间步骤的摘要,提供了最新 Spotify 发布内容的连贯、简洁概述:

近年来,音乐行业的趋势发生了显著变化,流媒体平台如 Spotify 的崛起以及嘻哈和电子舞曲(EDM)等流派的日益流行导致了这些变化。因此,Spotify 上发布的最新音乐反映了这些变化,呈现了多样化的艺术家和风格。

一个显著的趋势是葡萄牙语音乐的日益突出,比如 Dillaz 的“八重天”和 T-Rex 的“CASTANHO”在平台上占据了重要位置。这些专辑展示了葡萄牙音乐的独特声音和节奏,将传统风格与现代影响相结合。

另一个趋势是不同风格和背景的艺术家之间合作的日益受欢迎。例如,Don Toliver 的“Life of a DON”和 Pop Smoke 的“Faith”专辑中包含了与 Travis Scott、Kanye West、Rick Ross 和 Lil Tjay 等多位艺术家的合作。这些合作让艺术家们探索新的声音和风格,并创造出更广泛受众喜爱的音乐。

此外,Spotify 上的最新音乐反映了音乐风格和影响的日益多样化。像 Olivia Rodrigo 的“SOUR”和 Billie Eilish 的“Happier Than Ever”这样的专辑展示了年轻女性艺术家的独特声音和视角,为当代音乐提供了新鲜的视角。

如所示,我们基于 ChatGPT 的系统有效地总结了最新的 Spotify 发布内容,为音乐爱好者提供了一个易于访问和参与的概述,以便随时了解和发现新内容。

结论

在这篇文章中,我们展示了将 Spotify API 和 OpenAI 的 ChatGPT 结合起来创建总结系统的强大功能,该系统使您能够及时了解最新的音乐发行。我们讨论了文档链技术,选择了因其可扩展性而被广泛使用的 MapReduce 方法,并展示了我们系统在生成连贯且信息丰富的总结方面的有效性。

AI 驱动的语言模型与像 Spotify 这样的热门平台 API 之间的协同作用为创新和个性化开辟了新的机会。随着 AI 技术的不断发展,它们在各种 NLP 任务中的应用将不断扩展,为我们日常生活的提升提供令人兴奋的方式。

总之,我们的探索作为尖端 AI 技术在解决现实世界挑战和创造有价值用户体验方面的潜力的激励性示例。我们希望这篇文章能鼓励您在自己的项目中进一步探索 AI 的应用,并激励您创造出能够带来改变的创新解决方案。

保持联系:LinkedIn

4 个简单步骤让你的机器学习系统超充电

原文:towardsdatascience.com/super-charge-your-ml-systems-in-4-simple-steps-4485f0208440?source=collection_archive---------6-----------------------#2023-10-27

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

使用 DALL.E-3 生成的图像

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Donal Byrne

·

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

欢迎来到机器学习优化的过山车之旅!这篇文章将带你了解我的优化任何机器学习系统以实现闪电般快速训练和推理的 4 个简单步骤。

想象一下:你终于被分配到一个酷炫的新机器学习项目中,你正在训练你的智能体来统计照片中的热狗数量,其成功可能为你的公司带来数十美元的收入!

你在你最喜欢的框架中实现了最新的炙手可热的物体检测模型,该模型有很多 GitHub 星标,运行一些玩具示例,经过一个小时左右,它就像一个在大学第 3 年重修的穷学生一样准确地识别热狗,生活美好。

下一步显而易见,我们想将其扩展到更困难的问题,这意味着更多的数据、更大的模型,当然,还有更长的训练时间。现在你需要面对几天的训练时间,而不是几个小时。不过没关系,你已经忽视你的团队 3 周了,可能应该花一天时间处理积压的代码审查和被动攻击的电子邮件。

你在为你在同事的 MR 上留下的有见地且绝对必要的细节而感到满意的一天后回来,结果发现你的性能崩溃了,在经历了 15 小时的训练后(因果报应来得很快)。

接下来的几天变成了试验、测试和实验的旋风,每个潜在的想法都需要超过一天的运行时间。这些迅速开始积累数百美元的计算成本,所有这些都导致了一个大问题:我们如何才能让这一切变得更快、更便宜?

欢迎来到机器学习优化的情感过山车!这里有一个简单的 4 步流程,可以使局势对你有利:

  1. 基准测试

  2. 简化

  3. 优化

  4. 重复

这是一个迭代过程,很多时候你会在进行下一步之前重复某些步骤,所以这不只是一个 4 步系统,更像是一个工具箱,但 4 步听起来更好。

1 — 基准测试

“测量两次,切割一次”—某位智者

你应该始终做的第一件(可能也是第二件)事是对系统进行性能分析。这可以是简单地计时特定代码块运行所需的时间,也可以是复杂的全程性能跟踪。重要的是你有足够的信息来识别系统中的瓶颈。我根据我们在过程中所处的阶段进行多次基准测试,并通常将其分为两种类型:高层次和低层次基准测试。

高层次

这就是你会在每周“我们到底有多糟糕?”会议上向老板展示的内容,并希望这些指标成为每次运行的一部分。这些将给你一个关于系统性能的高层次感受。

Batches Per Second——我们每秒处理多少批次?这应该尽可能高。

Steps Per Second——(特指强化学习)我们在环境中生成数据的速度是多少,应该尽可能高。这里有一些复杂的步伐时间与训练批次之间的相互作用,我在这里不详细讨论。

GPU Util——在训练过程中你的 GPU 使用了多少?这应该始终接近 100%,如果不是,那么你有可以优化的空闲时间。

CPU Util——在训练过程中你的 CPU 使用了多少?同样,这应该尽可能接近 100%。

FLOPS——每秒浮点运算次数,这能让你了解你是如何有效利用总硬件的。

低层次

使用上述指标后,你可以进一步深入查看瓶颈可能出现在何处。一旦有了这些信息,你需要开始查看更细粒度的指标和分析。

时间分析 — 这是最简单且通常最有用的实验。像cprofiler这样的分析工具可以帮助你从整体上了解每个组件的时间消耗,或者查看特定组件的时间。

内存分析 — 另一个优化工具箱中的常见工具。大型系统需要大量内存,所以我们必须确保没有浪费内存!像memory-profiler这样的工具将帮助你缩小系统消耗 RAM 的范围。

模型分析 — 像Tensorboard这样的工具提供了优秀的分析工具,用于查看你的模型中哪些部分正在消耗性能。

网络分析 — 网络负载是导致系统瓶颈的常见原因。像wireshark这样的工具可以帮助你进行网络分析,但说实话,我从未使用过。相反,我更倾向于对我的组件进行时间分析,测量组件内部所需的总时间,然后隔离网络 I/O 本身所花费的时间。

确保查看这篇关于 Python 性能分析的优秀文章,RealPython,以获取更多信息!

2 — 简化

一旦你在性能分析中确定了需要优化的区域,就要进行简化。去除除该部分之外的所有内容。继续将系统简化为更小的部分,直到找到瓶颈。不要害怕在简化过程中进行性能分析,这将确保你在迭代过程中走在正确的方向上。继续重复这个过程,直到找到你的瓶颈。

提示

  • 用存根和模拟函数替换其他组件,这些存根和模拟函数仅提供预期的数据。

  • 使用sleep函数或虚拟计算来模拟重负载函数。

  • 使用虚拟数据以去除数据生成和处理的开销。

  • 从本地、单进程版本的系统开始,然后再转到分布式系统。

  • 在单台机器上模拟多个节点和演员,以去除网络开销。

  • 找出系统每个部分的理论最大性能。如果系统中所有其他瓶颈都消除了,除了这个组件,我们的预期性能是什么?

  • 再次进行性能分析!每次简化系统时,重新运行你的性能分析。

问题

一旦我们锁定了瓶颈,就有一些关键问题需要回答。

这个组件的理论最大性能是多少?

如果我们已经充分隔离了瓶颈组件,那么应该能够回答这些问题。

我们距离最大性能还有多远?

这个最优性差距将告诉我们系统的优化程度。现在,可能会出现其他硬性约束,一旦我们将组件重新引入系统中,这也是可以接受的,但至少要意识到这个差距。

是否存在更深层的瓶颈?

总是问自己这个问题,也许问题比你最初想到的更深层次,在这种情况下,我们需要重复基准测试和简化的过程。

3 — 优化

好的,我们已经识别出了最大的瓶颈,现在进入有趣的部分,我们怎么改进?通常我们应该关注 3 个可能的改进领域。

  1. 计算

  2. 通信

  3. 内存

计算

为了减少计算瓶颈,我们需要尽可能高效地使用数据和算法。这显然是项目特定的,有很多可以做的事情,但让我们来看一些好的经验法则。

并行化 — 确保尽可能多地进行并行工作。这是设计系统时第一个显著的胜利,可以大幅度提升性能。考虑使用向量化、批处理、多线程和多进程等方法。

缓存 — 尽可能地预计算和重用计算结果。许多算法可以利用预计算的值,从而节省每一步训练中的关键计算。

卸载 — 我们都知道 Python 速度不快。幸运的是,我们可以将关键计算卸载到低级语言如 C/C++。

硬件扩展 — 这有点偷懒,但当一切都失败时,我们总可以增加更多的计算机来解决问题!

通信

任何经验丰富的工程师都会告诉你,沟通是成功交付项目的关键,我们当然是指系统内部的沟通(天哪,我们希望不要跟同事交流)。一些好的经验法则包括:

无闲置时间 — 你所有可用的硬件必须始终被利用,否则你将错失性能提升。这通常是由于系统间通信的复杂性和开销所致。

保持本地化 — 在迁移到分布式系统之前,尽可能长时间地将所有内容保留在单台机器上。这使你的系统保持简单,同时避免了分布式系统的通信开销。

异步 > 同步 — 识别任何可以异步完成的任务,这将有助于通过在数据移动的同时保持工作进行,从而减轻通信的成本。

避免数据移动 — 将数据从 CPU 移动到 GPU 或从一个进程移动到另一个进程是昂贵的!尽量减少这种操作,或者通过异步方式减少其影响。

内存

最后但同样重要的是内存。上述许多领域可以帮助缓解瓶颈,但如果没有足够的内存,这可能是不可能的!让我们来看一些需要考虑的事项。

数据类型 — 保持这些尽可能小,有助于减少通信和内存成本,并且与现代加速器一起,它还会减少计算。

缓存 — 类似于减少计算,聪明的缓存可以帮助节省内存。然而,确保你的缓存数据使用频率足够高,以证明缓存的合理性。

预分配 — 在 Python 中我们不太习惯这样做,但严格进行内存预分配可以让你准确知道所需内存量,减少碎片化的风险,并且如果你能够写入共享内存,你将减少进程之间的通信!

垃圾回收 — 幸运的是,Python 处理了大部分这方面的工作,但重要的是确保你没有在作用域中保留不必要的大值,或者更糟的是,存在可能导致内存泄漏的循环依赖。

懒惰 — 仅在必要时评估表达式。在 Python 中,你可以使用生成器表达式代替列表推导式,以便进行惰性计算的操作。

4 — 重复

那么,我们什么时候才算完成呢?这真的取决于你的项目、需求是什么,以及在你渐渐崩溃之前需要多久!

随着你消除瓶颈,你在优化系统时投入的时间和精力将会得到递减的回报。在这个过程中,你需要决定何时“足够好”。记住,速度是实现目标的一种手段,不要陷入为优化而优化的陷阱。如果对用户没有影响,那么可能是时候继续前进了。

结论

构建大规模 ML 系统是困难的。这就像玩一个扭曲的“沃尔多在哪里”游戏,混合了《黑暗之魂》的元素。如果你真的找到问题,你必须进行多次尝试才能解决,而且你会花费大部分时间被虐待,问自己“我为什么要在周五晚上做这些?”。有一个简单且有原则的方法可以帮助你通过最终 boss 战,并品尝到那些甜美的理论最大 FLOPs。

## ML in Action | Donal Byrne | Substack

提供未经请求的建议、实用见解和在快速发展的领域中学到的经验的机器学习通讯…

donalbyrne.substack.com

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值