设置 Python 项目: 第六部分
原文:
towardsdatascience.com/setting-up-python-projects-part-vi-cbdbf28eff53
掌握 Python 项目设置的艺术: 一步一步的指南
·发布于 Towards Data Science ·阅读时间 26 分钟·2023 年 4 月 10 日
–
图片由 Amira El Fohail 提供,来源于 Unsplash
无论你是经验丰富的开发者还是刚刚开始接触🐍 Python,了解如何构建稳健且可维护的项目都是很重要的。本文教程将引导你完成使用一些行业内最受欢迎和有效的工具来设置 Python 项目的过程。你将学习如何使用 GitHub 和 GitHub Actions 进行版本控制和持续集成,以及其他用于测试、文档编写、打包和分发的工具。该教程灵感来源于 Hypermodern Python 和 新 Python 项目的最佳实践。然而,这并不是唯一的方法,你可能有不同的偏好或意见。教程旨在对初学者友好,同时也涵盖一些高级主题。在每个部分,你将自动化一些任务,并为你的项目添加徽章以展示你的进展和成就。
本系列的代码库可以在 github.com/johschmidt42/python-project-johannes 找到
要求
-
操作系统: Linux, Unix, macOS, Windows (WSL2,例如 Ubuntu 20.04 LTS)
-
工具: python3.10, bash, git, tree
-
版本控制系统 (VCS) 主机: GitHub
-
持续集成 (CI) 工具: GitHub Actions
预计你对版本控制系统 (VCS) git 已经熟悉。如果不熟悉,这里有一个复习资料:Git 入门介绍
提交将基于 最佳 git 提交实践 和 约定式提交。对于 PyCharm,有 约定式提交插件 或者 VSCode 扩展 可以帮助你按照这种格式撰写提交。
概述
-
第二部分 (格式化、Linting、CI)
-
第三部分 (测试、CI)
-
第六部分 (容器化、Docker、CI/CD)
结构
-
容器化
-
Docker
-
Dockerfile
-
Docker 镜像
-
Docker 容器
-
Docker 阶段 (基础、构建、生产)
-
容器注册中心 (ghcr.io)
-
Docker 推送
-
CI (build.yml & build_and_push.yml)
-
徽章 (构建)
-
奖励 (trivy)
在这篇文章中,我们将深入探讨容器化的概念及其好处,以及如何与Docker结合使用,以创建和管理容器化应用。我们将使用GitHub Actions来持续构建 Docker 镜像并在发布新版本时将其上传到我们的仓库。
容器化
容器化是一项现代技术,它彻底改变了软件应用的开发、部署和管理方式。近年来,由于它能够解决软件开发和部署中的一些重大挑战,已获得广泛采用。
简单来说,容器化是将应用程序及其所有依赖项打包成一个单一的容器的过程。这个容器是一个轻量、可移植、自给自足的单元,可以在不同的计算环境中一致地运行。它为应用程序提供了一个隔离的环境,确保它在任何底层基础设施下都能一致运行。它使开发者能够创建可扩展、可移植且易于管理的应用程序。此外,容器通过将应用程序与宿主系统隔离,提供了额外的安全层。如果你听到有人说“它在我的电脑上能工作”,这已经不再有效,因为你可以并且应该在 Docker 容器中测试你的应用。这确保了它在不同环境中的一致性。
总之,容器化是一项强大的技术,它允许开发者创建可靠、高效且易于管理的容器化应用,使他们能够专注于开发优秀的软件。
Docker
Docker 是一个流行的容器化平台,允许开发人员创建、部署和运行容器化应用程序。它提供了一系列工具和服务,使得将应用程序打包和部署为容器化格式变得简单。使用 Docker,开发人员可以在几分钟内创建、测试和部署应用程序,而不是几天或几周。
要使用 docker 创建这样的容器化应用程序,我们需要
-
从Dockerfile构建一个Docker 镜像
-
从 Docker 镜像创建一个容器
为此,我们将使用 docker CLI。
Dockerfile
Dockerfile 是一个包含所有构建给定镜像所需命令的文本文件。它遵循特定的格式和指令集,你可以在这里找到相关信息。
本节的目标是创建一个构建我们 Python 包的 wheel 文件的 Dockerfile:
**FROM python:3.10-slim**
**WORKDIR /app** # install poetry **ENV POETRY_VERSION=1.2.0
RUN pip install "poetry==$POETRY_VERSION"** # copy application **COPY ["pyproject.toml", "poetry.lock", "README.md", "./"]
COPY ["src/", "src/"]** # build wheel **RUN poetry build --format wheel** # install package **RUN pip install dist/*.whl**
这个 Dockerfile 本质上是一组指令,告诉 Docker 如何为 Python 应用程序构建一个容器。它以python:3.10-slim
作为基础镜像开始,这是一种已经预装了一些基本库和依赖项的 Python 3.10 精简版镜像。
第一个指令WORKDIR /app
将工作目录设置为容器内的/app
,应用程序将被放置在此目录中。
下一个指令ENV POETRY_VERSION=1.2.0
设置一个名为POETRY_VERSION
的环境变量为1.2.0
,此变量将在下一条命令中用于安装 Poetry 包管理器。
RUN pip install "poetry==$POETRY_VERSION"
命令在容器内安装 Poetry 包管理器,用于管理 Python 应用程序的依赖项。
下一个指令COPY ["pyproject.toml", "poetry.lock", "README.md", "./"]
将项目文件(包括pyproject.toml
、poetry.lock
和README.md
)复制到容器中。
README.md
文件是必需的,因为在 pyproject.toml 中有引用。没有它,我们将无法构建 wheel。
指令COPY ["src/", "src/"]
将应用程序的源代码复制到容器中。
RUN poetry build --format wheel
命令使用poetry.lock
文件和应用程序的源代码为 Python 应用程序构建一个Python wheel包。
最后,最后一条指令RUN pip install dist/*.whl
使用pip
安装包,并安装位于dist
目录中的生成的.whl
包文件。
总之,这个 Dockerfile 设置了一个包含 Python 3.10 和已安装 Poetry 的容器,复制了应用程序源代码和依赖项,构建了一个包 wheel 并安装它。
这还不会运行应用程序。但不用担心,我们将在接下来的章节中更新它。我们必须首先了解使用 Docker 的流程。
Docker 镜像
我们已经创建了一个 Dockerfile,其中包含构建 Docker 镜像的指令。为什么我们需要 Docker 镜像?因为它允许我们构建 Docker 容器!
让我们运行 docker build 命令来创建我们的镜像:
**> docker build --file Dockerfile --tag project:latest .**
...
=> [7/7] RUN pip install dist/*.whl 30.7s
=> exporting to image 0.5s
=> => exporting layers 0.5s
=> => writing image sha256:bb2acf440f4cf24ac00f051b1deaaefaf4e41b87aa26c34342cbb6faf6b55591 0.0s
=> => naming to docker.io/library/project:latest
此命令用于从 Dockerfile 构建 Docker 镜像,并使用指定的名称和版本标记它。让我们来解析一下命令:
-
docker build
:这是用于构建 Docker 镜像的命令。 -
--file Dockerfile
:此选项指定用于构建镜像的 Dockerfile 的路径和名称。在这种情况下,它被简单地命名为Dockerfile
,所以它使用了默认名称。 -
--tag project:latest
:此选项指定要创建的镜像的名称和版本。在这种情况下,镜像名称为project
,版本为latest
。project
是给镜像的名称,而latest
是版本号。你可以用你选择的名称和版本替换project
和latest
。 -
.
:此选项指定了构建上下文,即用于构建镜像的文件位置。在这种情况下,.
指当前执行命令的目录。
因此,当执行此命令时,Docker 会读取当前目录中的 Dockerfile,并使用它来构建一个名为 project:latest
的新镜像。我们可以通过运行以下命令找到有关结果镜像(及其他镜像)的更多信息:
**> docker images**
REPOSITORY TAG IMAGE ID CREATED SIZE
project latest bb2acf440f4c 2 minutes ago 271MB
我们的镜像大小为 271 mb。大小将在后续减少。
Docker 容器
我们可以使用 docker run
命令从 Docker 镜像创建/运行一个 Docker 容器。该命令需要一个参数,即镜像的名称。例如,如果你的镜像名为 myimage
,你可以使用以下 命令 运行它:docker run myimage
如果我们像这样运行我们的应用:
**> docker run -it --rm project:latest**
它将打开一个 Python 终端(你可以使用 CTRL + D 或 CMD + D 关闭会话;-it
选项用于以交互模式运行容器,并提供伪终端(终端仿真)。这允许你与容器的 shell 进行交互,并实时查看其输出。-rm
选项用于在容器退出时自动删除容器。)
Python 3.10.10 (main, Mar 23 2023, 03:59:34) [GCC 10.2.1 20210110] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>
为什么它会打开一个 Python 会话?这是因为 Docker 镜像的 entrypoint 默认指向标准 python:3.10-slim 镜像中的 Python 解释器。如果我们想查看容器内部,我们必须覆盖 entrypoint。因为 bash 默认安装在此构建中,我们可以通过以下命令运行 Docker 容器并进入其中:
**> docker run -it --rm project:latest /bin/bash**
root@76eb4cb2d8fb:/app#
所以我们用 /bin/bash 覆盖了 entrypoint。
现在我们可以检查容器内部的内容:
app
├── README.md
├── dist
│ └── example_app-0.3.0-py3-none-any.whl
├── poetry.lock
├── pyproject.toml
└── src
└── example_app
我们可以使用以下命令检查已安装的包:
**> pip freeze**
...
dulwich==0.20.50
**example-app** @ file:///app/dist/example_app-0.3.0-py3-none-any.whl
fastapi==0.85.2
...
太好了,我们可以进入容器,这对于故障排除非常有用。但我们如何让它运行我们的应用程序?我们的应用程序安装在哪里?默认情况下,包可以在 Python 安装的site-packages 目录中找到。要获取这些信息,我们可以使用pip show 命令:
**> pip show example-app**
Name: example-app
Version: 0.3.0
Summary:
Home-page: https://github.com/johschmidt42/python-project-johannes
Author: Johannes Schmidt
Author-email: johannes.schmidt.vik@gmail.com
License: MIT
Location: **/usr/local/lib/python3.10/site-packages**
Requires: fastapi, httpx, uvicorn
Required-by:
由于uvicorn(我们的 ASGI 服务器实现)默认安装,我们可以cd 进入 /usr/local/lib/python3.10/site-packages/example_app
并使用uvicorn 命令运行应用程序:
**> uvicorn app:app --host 0.0.0.0 --port 80 --workers 1**
INFO: Started server process [17]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Uvicorn running on http://0.0.0.0:80 (Press CTRL+C to quit)
其中 app:app
遵循 <file_name>:<variable_name>
模式。
应用程序在 docker 容器中运行在端口 80,并使用1 个工作进程。为了在主机(你的机器)上访问,我们需要暴露容器端口并将其发布到主机。这可以通过在 docker run 命令中添加 --expose
和 --publish
标志来完成。或者,我们可以通过在Dockerfile中定义某个端口来让容器暴露该端口。我们稍后会做这个。之前,我们要做的是:
我们的应用程序可以在 site-packages 目录中找到。这要求我们在运行 uvicorn app:app
命令之前更改目录。如果我们想避免更改目录,我们可以创建一个文件来为我们导入应用程序。以下是一个示例:
添加一个 main.py
:
# main.py
from example_app.app import app
if __name__ == '__main__':
print(app.title)
在 main.py
中导入应用程序,以便 uvicorn 可以使用它。如果我们现在将这个文件复制到我们的 /app
目录:
# Dockerfile
...
**COPY ["main.py", "./"]**
...
我们可以运行应用程序
**> uvicorn main:app --host 0.0.0.0 --port 80 --workers 1**
INFO: Started server process [8]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Uvicorn running on http://0.0.0.0:80 (Press CTRL+C to quit)
太好了。现在让我们将这个命令设置为启动容器时的入口点。
FROM python:3.10-slim
WORKDIR /app
# install poetry
ENV POETRY_VERSION=1.2.0
RUN pip install "poetry==$POETRY_VERSION"
# copy application
COPY ["pyproject.toml", "poetry.lock", "README.md", "**main.py**", "./"]
COPY ["src/", "src/"]
# build wheel
RUN poetry build --format wheel
# install package
RUN pip install dist/*.whl
# expose port
**EXPOSE 80**
# command to run
**CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "80", "--workers", "1"]**
现在我们将 main.py
文件复制到 /app
目录。EXPOSE
指令通知 Docker 容器在运行时监听指定的网络端口。在这种情况下,它暴露了端口 80。
CMD
指令指定了在容器内运行的命令。在这里,它运行命令 uvicorn main:app --host 0.0.0.0 --port 80 --workers 1
。这个命令启动了一个 uvicorn 服务器,运行 main:app
应用程序,监听主机 0.0.0.0
和端口 80
,使用 1
个工作进程。
然后我们可以使用docker run 命令运行容器:
> **docker run -p 9000:80 -it --rm project:latest**
[2023-01-30 21:04:33 +0000] [1] [INFO] Starting gunicorn 20.1.0
[2023-01-30 21:04:33 +0000] [1] [INFO] Listening at: http://0.0.0.0:80 (1)
[2023-01-30 21:04:33 +0000] [1] [INFO] Using worker: uvicorn.workers.UvicornWorker
[2023-01-30 21:04:33 +0000] [7] [INFO] Booting worker with pid: 7
[2023-01-30 21:04:34 +0000] [7] [INFO] Started server process [7]
[2023-01-30 21:04:34 +0000] [7] [INFO] Waiting for application startup.
[2023-01-30 21:04:34 +0000] [7] [INFO] Application startup complete.
docker run
命令中的 -p
标志用于将容器的端口发布到主机。在这种情况下,它将主机上的端口 9000
映射到容器上的端口 80
。这意味着任何发送到主机端口 9000
的流量都会被转发到容器端口 80
。
我们可以看到在容器中运行的应用程序可以被访问:
运行在 Docker 容器中的 fastAPI 应用程序 — 作者图片
重要备注:我建议在生产构建中使用 gunicorn 代替 uvicorn!为了完整性,这里是 Dockerfile 的替代版本:
FROM python:3.10-slim
WORKDIR /app
# install poetry
ENV POETRY_VERSION=1.2.0
RUN pip install "poetry==$POETRY_VERSION"
# install gunicorn (ASGI web implementation)
**RUN pip install gunicorn==20.1.0**
# copy application
COPY ["pyproject.toml", "poetry.lock", "README.md", "./"]
COPY ["src/", "src/"]
# build wheel
RUN poetry build --format wheel
# install package
RUN pip install dist/*.whl
# expose port
EXPOSE 80
# command to run
CMD ["**gunicorn**", "main:app", "**--bind**", "**0.0.0.0:80**", "--workers", "1", "**--worker-class", "uvicorn.workers.UvicornWorker**"]
这两个有什么区别?
Uvicorn 是一个支持 ASGI 协议的 ASGI 服务器。它建立在 uvloop 和 httptools 之上,并以其性能优势而闻名。然而,它作为进程管理器的能力还有待提高。
Gunicorn 另一方面是一个成熟且功能全面的服务器和进程管理器。它是从 Ruby 的 Unicorn 项目移植而来的预叉工人模型,并与各种 web 框架广泛兼容。
Docker 阶段
Docker 阶段是一项功能,允许你在 Dockerfile 中创建多个阶段。每个阶段可以有自己的 基础镜像 和指令集。你可以选择性地将工件从一个阶段复制到另一个阶段,留下你不想要的内容。此功能很有用,因为它可以通过减少 Docker 镜像的大小和复杂性来优化 Docker 镜像。
使用 Docker 阶段,我们可以(并且应该!)优化我们的 Docker 镜像。所以我们想要实现的是:
-
poetry 不应该出现在生产构建中
-
生产构建应该仅包含运行应用程序所需的最少内容
这就是我们要做到的方法:我们创建一个干净的 基础 阶段。从基础阶段,我们有一个 构建 阶段,安装 poetry 并构建 wheel。另一个阶段,生产, 可以从构建阶段复制这个工件(.whl 文件)并使用它。这样我们可以避免在生产构建中安装 poetry,同时也将其限制为仅包含必要内容,从而减少最终镜像的大小。
关于 Docker 中的 poetry
我见过不同的策略将 poetry 与 Docker 结合使用。
-
创建一个 虚拟环境,然后将整个 venv 从一个阶段复制到另一个阶段。
-
从 poetry.lock 文件创建 requirements.txt 文件,并使用这些文件通过 pip 安装要求。
在第一种情况下,Poetry 在构建镜像时安装。在第二种情况下,Poetry 不在 Docker 构建中安装,但需要使用 Poetry 来创建 requirements.txt 文件。
在这两种情况下,我们需要以某种方式安装 Poetry——无论是在 Docker 镜像中还是在运行 Docker 构建命令的主机上。
将 Poetry 放入 Docker 中会稍微增加构建时间,而将其放在 Docker 之外则需要你在主机上安装 Poetry,并为构建过程添加额外步骤(从 poetry.lock 创建 requirements.txt 文件)。在 Docker 构建 CI 流水线 的上下文中,主机上的 Poetry 安装可以被缓存,构建过程通常会更快。这两种方法都有其优缺点,最佳方法将取决于你的具体需求和偏好。
为了本教程的目的,我将保持简单,使用上面描述的 venv 策略。所以这是新的 Dockfile,其中包含阶段(为了识别不同的阶段,通过 FROM 语句分隔,我将这些行用 粗体 高亮):
**FROM python:3.10-slim as base**
WORKDIR /app
# ignore 'Running pip as the root user...' warning
ENV PIP_ROOT_USER_ACTION=ignore
# update pip
RUN pip install --upgrade pip
**FROM base as builder**
# install poetry
ENV POETRY_VERSION=1.3.1
RUN pip install "poetry==$POETRY_VERSION"
# copy application
COPY ["pyproject.toml", "poetry.lock", "README.md", "./"]
COPY ["src/", "src/"]
# build wheel
RUN poetry build --format wheel
**FROM base as production**
# expose port
EXPOSE 80
# copy the wheel from the build stage
COPY --from=builder /app/dist/*.whl /app/
# install package
RUN pip install /app/*.whl
# copy entrypoint of the app
COPY ["main.py", "./"]
# command to run
CMD ["uvicorn", "main:app","--host", "0.0.0.0", "--port", "80", "--workers", "1"]
这个 Dockerfile 定义了一个包含三个阶段的多阶段构建:base
、builder
和 production
。
-
base
阶段从 Python 3.10-slim 镜像开始,将工作目录设置为/app
。它还设置了一个环境变量以忽略关于以 root 用户身份运行 pip 的警告,并将 pip 更新到最新版本。 -
builder
阶段从base
阶段开始,并使用 pip 安装 Poetry。然后,它复制应用程序文件,并使用 Poetry 为应用程序构建一个 wheel 文件。 -
production
阶段再次从base
阶段开始,并暴露端口 80。它复制在builder
阶段构建的 wheel 文件,并使用 pip 安装。它还复制应用程序的入口点,并将命令设置为使用 uvicorn 运行应用程序。
我们现在可以使用以下命令重新构建我们的 Docker 镜像:
**> docker build --file Dockerfile --tag project:latest --target production .**
我们可以使用 --target
标志指定我们希望构建的阶段。
文件大小现在减少了 ~70 Mb,总共为 197MB:
**> docker images**
REPOSITORY TAG IMAGE ID CREATED SIZE
project latest f1be09c32a55 14 minutes ago **197MB**
我们可以使用以下命令运行它:
**> docker run -p 9000:80 -it --rm project:latest**
API 将在浏览器中通过 localhost:9000
提供。
fastAPI 应用程序在 Docker 容器中运行 — 作者图片
容器注册表
容器注册表是用于存储和访问容器镜像的仓库或仓库集合。容器注册表可以支持基于容器的应用程序开发,通常作为 DevOps 过程的一部分。它们可以直接连接到像 Docker 和 Kubernetes 这样的容器编排平台。
最受欢迎的容器注册表是 Docker Hub。每个云服务提供商都有自己的注册表。Azure 的 ACR、AWS 的 ECR 以及更多。GitHub 有一个名为 GitHub Packages 的包注册解决方案。
由于我们到目前为止基本上都在 GitHub 上完成了所有操作,所以在本教程中我们将使用 GitHub Packages。
GitHub Packages — 作者图片
GitHub 为普通用户提供了免费的层级。这允许我们为我们的容器使用最多 500 MB 的存储空间。这对我们的应用程序来说足够了。
GitHub Packages 定价和免费层 — 作者图片
Docker push
docker push
命令用于 上传 Docker 镜像到容器注册表。这允许你与其他人分享你的镜像或将其部署到不同的环境中。该命令需要你指定要推送的镜像名称和要推送到的注册表名称作为参数。在推送镜像之前,你需要登录到注册表。
这是将 Docker 镜像推送到容器注册表的步骤:
-
标记(重命名)你的镜像,使用注册表名称:
docker tag project:latest <registry-name>/<project>:latest
-
登录到容器注册表:
docker login <registry-url>
-
推送你的镜像到注册表:
docker push <registry-name>/<project>:latest
我们将把镜像推送到GitHub Packages:
GitHub Packages
GitHub Packages 仅支持使用个人访问令牌进行身份验证(2023 年 2 月)。但我们在第五部分创建了一个个人访问令牌(PAT),所以我们也可以在这里使用它。
我们需要登录到容器注册表:
> CR_PAT="XYZ"
> echo $CR_PAT | docker login ghcr.io -u johschmidt42 --password-stdin
Login Succeeded
这是一个 shell 命令,使用管道将两个命令连接起来。管道是一个符号(|
),它将一个命令的输出重定向到另一个命令的输入。在这种情况下,第一个命令是 echo $(CR_PAT)
,它将 CR_PAT 变量的值打印到标准输出。第二个命令是 docker login ghcr.io -u johschmidt42 --password-stdin
,它使用 johschmidt42 作为用户名,并从标准输入读取密码来登录 ghcr.io。通过使用管道,echo 命令的输出成为 docker login 命令的输入,这意味着 CR_PAT 变量的值被用作登录密码。
让我们把它添加到我们的 Makefile 中
# Makefile
...
login: ## login to ghcr.io using a personal access token (PAT)
@if [ -z "$(CR_PAT)" ]; then\
echo "CR_PAT is not set";\
else\
echo $(CR_PAT) | docker login ghcr.io -u johschmidt42 --password-stdin;\
fi
...
我们需要在bash中写一个小的 if-else 语句,以便这个目标登录需要我们首先设置CR_PAT。
这使我们现在可以像这样登录:
> **make login CR_PAT="XYZ"**
对于任何对 bash 命令感到困惑的人,这里有一个解释:
这个 shell 命令使用 if-else 语句来检查一个条件,并根据不同的条件执行不同的操作。条件是 [ -z "$(CR_PAT)" ]
,这意味着“CR_PAT 变量为空吗?” -z
标志测试零长度。$(CR_PAT)
部分在括号内展开 CR_PAT 变量的值。如果条件为真,那么 then
后的操作会被执行,即 echo "CR_PAT is not set"
。这将一条消息打印到标准输出。如果条件为假,则执行 else
后的操作,即 echo $(CR_PAT) | docker login ghcr.io -u johschmidt42 --password-stdin
。每行末尾的 \
意味着命令继续在下一行。末尾的 fi
标志着 if-else 语句的结束。
现在我们已登录,我们需要重命名 docker 文件,以便使用 docker tag 命令将其推送到远程注册表:
> **docker tag project:latest ghcr.io/johschmidt42/project:latest**
# Makefile
...
tag: ## tag docker image to ghcr.io/johschmidt42/project:latest
@docker tag project:latest ghcr.io/johschmidt42/project:latest
...
我们可以通过以下命令查看有关我们的 docker 镜像的信息:
**> docker images**
REPOSITORY TAG IMAGE ID CREATED SIZE
project latest f1be09c32a55 About an hour ago 197MB
ghcr.io/johschmidt42/project latest f1be09c32a55 About an hour ago 197MB
如果我们现在尝试将镜像推送到注册表,它会失败:
> **docker push ghcr.io/johschmidt42/project:latest**
denied: permission_denied: The token provided does not match expected scopes.
# Makefile
...
push: tag ## docker push to container registry (ghcr.io)
@docker push ghcr.io/johschmidt42/project:latest
...
这是因为我们的令牌没有预期的范围。消息没有告诉我们需要哪些范围(权限),但我们可以在文档中找到这些信息。
所以我们需要添加这些范围:
-
read:packages
-
delete:packages
GH_TOKEN — 作者提供的图片
现在我们看到它被推送到容器注册表:
> **make push**
1a3ba1c1448c: Pushed
0ad139eaf32a: Pushing [========================================> ] 43.3MB/54.08MB
0e0b5d4aea1e: Pushed
a179cef7de6a: Pushing [==================================================>] 18.15MB
22f1e17dcfe4: Pushed
805fe34ec92b: Pushing [==================================================>] 12.76MB
fa04dee82d1b: Pushed
42d55226bf51: Pushing [==================================================>] 30.83MB
7d13900c8624: Pushed
650abce4b096: Pushing [==============> ] 22.72MB/80.51MB
latest: digest: sha256:57d409bb564f465541c2529e77ad05a02f09e2cc22b3c38a93967ce1b277f58a size: 2414
在 GitHub 中,profile
下的packages
标签中现在有一个 docker 镜像:
你的个人资料 — 作者提供的图片
GitHub packages — 作者提供的图片
点击它,可以将包连接到我们的仓库:
GitHub packages: 连接仓库 — 作者提供的图片
现在,这个 docker 镜像可以在仓库的首页找到 github.com/johschmidt42/python-project-johannes:
GitHub Packages 首页 — 作者提供的图片
很好。我们已经创建了一个 Docker 镜像,将其推送到远程仓库,链接到我们当前的版本,现在每个人都可以通过运行 docker pull 命令来测试我们的应用:
**> docker pull ghcr.io/johschmidt42/python-project-johannes:v0.4.1**
CI/CD:
CI/CD 代表持续集成和持续部署。通过 Docker 镜像,CI/CD 可以自动化构建、测试和部署镜像的过程。在本教程中,我们将重点关注持续构建我们的 Docker 镜像并在有新版本时将其推送到远程容器注册表(CI)。然而,在本教程中,我们不会部署镜像(CD)(请关注未来的博客文章)。我们的 Docker 容器将在以下情况构建:
-
提交到一个有打开的 PR 的分支
-
提交到默认分支(main)
-
创建一个新版本(这会将镜像推送到容器注册表)
第一个动作帮助我们及早发现错误。第二个动作使我们能够在 README.md 文件中创建并使用徽章。最后一个动作创建 Docker 镜像的新版本并将其推送到容器注册表。整体动作流程总结如下:
GitHub Actions 流程 — 作者提供的图片
让我们创建build
管道:
这个 GitHub Actions 工作流构建了一个 Docker 镜像。当有push或pull request到main分支时,或者当工作流被调用时,它会被触发。这个工作是命名为“Build”,包含两个步骤。第一步使用actions/checkout
动作检出仓库。第二步通过运行make build
命令来构建 Docker 镜像。就是这样。
工作流运行 — 作者提供的图片
我们还需要相应地更新 orchestrator.yml
:
当我们推送到 main
分支时,会触发 orchestrator。
orchestrator.yml — 图片由作者提供
为了在我们的 GitHub 存储库中发布每个 新版本 时构建新的 Docker 镜像,我们需要创建一个新的 GitHub Actions 工作流:
这是一个 GitHub Actions 工作流,当发布版本时,它会构建并推送 Docker 镜像到 GitHub Container Registry (ghcr.io)。名为“build_and_push”的作业有三个步骤。第一步使用 actions/checkout
操作检出存储库。第二步使用 docker/login-action
登录到 GitHub Container Registry。第三步使用 docker/build-push-action
构建并推送 Docker 镜像。
build_and_push — 图片由作者提供
请注意,为了使用 docker/login-action@v2 登录到 GitHub Container Registry,我们需要提供 GH_TOKEN 这个 PAT,正如我们在 Part V 中定义的那样。
以下是最后一步中使用的参数的简要说明 docker/build-push-action@4:
-
context: .
指定构建上下文为当前目录。 -
push: true
指定在构建后将图像推送到注册表。 -
tags: ghcr.io/${{ github.repository }}:${{ github.ref_name }}
指定图像的标签。在这种情况下,它使用存储库的名称和触发工作流的分支或标签名称进行标记。 -
labels:
指定图像的标签。在这种情况下,它设置了图像的源、标题和版本标签。 -
target: production
指定在多阶段 Dockerfile 中构建的目标阶段。 -
github-token: ${{ secrets.GH_TOKEN }}
指定用于认证的 GitHub 令牌。
我们可以在 GitHub 上看到我们的新 Docker 镜像:
GitHub 上的图像 — 图片由作者提供
徽章:
对于这部分,我们将像以前一样向我们的 repo 添加一个徽章。这一次是针对 构建 管道的。当我们点击 build.yml 工作流运行时,可以检索徽章:
创建状态徽章 — 图片由作者提供
从 GitHub 上的工作流文件创建状态徽章
复制状态徽章 Markdown — 图片由作者提供
并选择主分支。徽章的 Markdown 可以复制并添加到 README.md 中:
我们的 GitHub 登陆页面现在看起来是这样的 ❤:
README.md 中的第五个徽章:构建 — 图片由作者提供
如果你想了解如何神奇地显示 main 中最后一次管道运行的当前状态,请查看 GitHub 上的提交 statuses API。
这就结束了教程的核心部分!我们成功创建了一个Dockerfile,并使用它构建了一个Docker 镜像,使我们能够在Docker 容器中运行我们的应用程序。此外,我们实施了一个CI/CD 管道,自动构建我们的 Docker 镜像并将其推送到容器注册表。最后,我们在 README.md 文件中添加了一个徽章,向世界展示我们功能齐全的构建管道!
这就是最后一部分!这个教程是否帮助你在 GitHub 上构建了一个 Python 项目?有任何改进建议吗?让我知道你的想法!
[## 通过我的推荐链接加入 Medium - Johannes Schmidt
阅读 Johannes Schmidt 的每个故事(以及 Medium 上的其他成千上万名作者的故事)。你的会员费直接…
johschmidt42.medium.com](https://johschmidt42.medium.com/membership?source=post_page-----cbdbf28eff53--------------------------------)
奖励
清理:
以下是使用 Docker CLI 时的一些有用命令:
要停止所有容器并删除它们:
> docker stop $(docker ps -a -q) && docker rm $(docker ps -a -q)
要删除所有未使用的 Docker 镜像:
> docker rmi $(docker images --filter "dangling=true" -q --no-trunc)
Docker 镜像中的漏洞扫描
漏洞扫描是确保 Docker 镜像安全性的关键步骤。它帮助你识别并修复可能会危害应用程序或数据的潜在弱点或风险。其中一个可以帮助你的工具是 trivy。
这个开源工具是一个简单快速的Docker 镜像漏洞扫描器,支持多种格式和来源。我将演示如何在本地使用它。理想情况下,你应该考虑创建一个 GitHub Actions 工作流,每当你构建 Docker 镜像时都运行!
我们首先应根据 文档 安装 trivy。在构建生产 Docker 镜像后
> docker build --file Dockerfile --tag project:latest --target production .
我们可以用以下命令扫描构建的镜像
> **trivy image project:latest --scanners vuln --format table --severity CRITICAL,HIGH**
这将从数据库下载已知的最新漏洞并扫描镜像。输出将以表格形式--format table
显示,仅包含严重性为 CRITICAL 或 HIGH 的发现--severity CRITICAL,HIGH
:
project:latest (debian 12.0)
Total: 27 (HIGH: 27, CRITICAL: 0)
┌────────────────┬────────────────┬──────────┬───────────────────┬───────────────┬──────────────────────────────────────────────────────────────┐
│ Library │ Vulnerability │ Severity │ Installed Version │ Fixed Version │ Title │
├────────────────┼────────────────┼──────────┼───────────────────┼───────────────┼──────────────────────────────────────────────────────────────┤
│ linux-libc-dev │ CVE-2013-7445 │ **HIGH** │ 6.1.27-1 │ │ kernel: memory exhaustion via crafted Graphics Execution │
│ │ │ │ │ │ Manager (GEM) objects │
│ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2013-7445 │
│ ├────────────────┤ │ ├───────────────┼──────────────────────────────────────────────────────────────┤
│ │ CVE-2019-19449 │ │ │ │ kernel: mounting a crafted f2fs filesystem image can lead to │
│ │ │ │ │ │ slab-out-of-bounds read... │
│ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2019-19449 │
│ ├────────────────┤ │ ├───────────────┼──────────────────────────────────────────────────────────────┤
│ │ CVE-2019-19814 │ │ │ │ kernel: out-of-bounds write in __remove_dirty_segment in │
│ │ │ │ │ │ fs/f2fs/segment.c │
│ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2019-19814 │
│ ├────────────────┤ │ ├───────────────┼──────────────────────────────────────────────────────────────┤
│ │ CVE-2021-3847 │ │ │ │ low-privileged user privileges escalation │
│ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2021-3847 │
│ ├────────────────┤ │ ├───────────────┼──────────────────────────────────────────────────────────────┤
│ │ CVE-2021-3864 │ │ │ │ descendant's dumpable setting with certain SUID binaries │
│ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2021-3864 │
│ ├────────────────┤ │ ├───────────────┼──────────────────────────────────────────────────────────────┤
│ │ CVE-2023-1194 │ │ │ │ use-after-free in parse_lease_state() │
│ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2023-1194 │
│ ├────────────────┤ │ ├───────────────┼──────────────────────────────────────────────────────────────┤
│ │ CVE-2023-2124 │ │ │ 6.1.37-1 │ OOB access in the Linux kernel's XFS subsystem │
│ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2023-2124 │
│ ├────────────────┤ │ │ ├──────────────────────────────────────────────────────────────┤
│ │ CVE-2023-2156 │ │ │ │ IPv6 RPL protocol reachable assertion leads to DoS │
│ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2023-2156 │
│ ├────────────────┤ │ ├───────────────┼──────────────────────────────────────────────────────────────┤
│ │ CVE-2023-2176 │ │ │ │ Slab-out-of-bound read in compare_netdev_and_ip │
│ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2023-2176 │
│ ├────────────────┤ │ ├───────────────┼──────────────────────────────────────────────────────────────┤
│ │ CVE-2023-3090 │ │ │ 6.1.37-1 │ out-of-bounds write caused by unclear skb->cb │
│ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2023-3090 │
│ ├────────────────┤ │ ├───────────────┼──────────────────────────────────────────────────────────────┤
│ │ CVE-2023-31248 │ │ │ │ use-after-free in nft_chain_lookup_byid() │
│ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2023-31248 │
│ ├────────────────┤ │ ├───────────────┼──────────────────────────────────────────────────────────────┤
│ │ CVE-2023-32247 │ │ │ 6.1.37-1 │ session setup memory exhaustion denial-of-service │
│ │ │ │ │ │ vulnerability │
│ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2023-32247 │
│ ├────────────────┤ │ │ ├──────────────────────────────────────────────────────────────┤
│ │ CVE-2023-32248 │ │ │ │ tree connection NULL pointer dereference denial-of-service │
│ │ │ │ │ │ vulnerability │
│ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2023-32248 │
│ ├────────────────┤ │ │ ├──────────────────────────────────────────────────────────────┤
│ │ CVE-2023-32250 │ │ │ │ session race condition remote code execution vulnerability │
│ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2023-32250 │
│ ├────────────────┤ │ │ ├──────────────────────────────────────────────────────────────┤
│ │ CVE-2023-32252 │ │ │ │ session NULL pointer dereference denial-of-service │
│ │ │ │ │ │ vulnerability │
│ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2023-32252 │
│ ├────────────────┤ │ │ ├──────────────────────────────────────────────────────────────┤
│ │ CVE-2023-32254 │ │ │ │ tree connection race condition remote code execution │
│ │ │ │ │ │ vulnerability │
│ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2023-32254 │
│ ├────────────────┤ │ │ ├──────────────────────────────────────────────────────────────┤
│ │ CVE-2023-32257 │ │ │ │ session race condition remote code execution vulnerability │
│ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2023-32257 │
│ ├────────────────┤ │ │ ├──────────────────────────────────────────────────────────────┤
│ │ CVE-2023-32258 │ │ │ │ session race condition remote code execution vulnerability │
│ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2023-32258 │
│ ├────────────────┤ │ │ ├──────────────────────────────────────────────────────────────┤
│ │ CVE-2023-3268 │ │ │ │ out-of-bounds access in relay_file_read │
│ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2023-3268 │
│ ├────────────────┤ │ │ ├──────────────────────────────────────────────────────────────┤
│ │ CVE-2023-3269 │ │ │ │ distros-[DirtyVMA] Privilege escalation via │
│ │ │ │ │ │ non-RCU-protected VMA traversal │
│ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2023-3269 │
│ ├────────────────┤ │ │ ├──────────────────────────────────────────────────────────────┤
│ │ CVE-2023-3390 │ │ │ │ UAF in nftables when nft_set_lookup_global triggered after │
│ │ │ │ │ │ handling named and anonymous sets... │
│ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2023-3390 │
│ ├────────────────┤ │ ├───────────────┼──────────────────────────────────────────────────────────────┤
│ │ CVE-2023-3397 │ │ │ │ slab-use-after-free Write in txEnd due to race condition │
│ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2023-3397 │
│ ├────────────────┤ │ ├───────────────┼──────────────────────────────────────────────────────────────┤
│ │ CVE-2023-35001 │ │ │ │ stack-out-of-bounds-read in nft_byteorder_eval() │
│ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2023-35001 │
│ ├────────────────┤ │ ├───────────────┼──────────────────────────────────────────────────────────────┤
│ │ CVE-2023-35788 │ │ │ 6.1.37-1 │ out-of-bounds write in fl_set_geneve_opt() │
│ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2023-35788 │
│ ├────────────────┤ │ ├───────────────┼──────────────────────────────────────────────────────────────┤
│ │ CVE-2023-35827 │ │ │ │ race condition leading to use-after-free in ravb_remove() │
│ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2023-35827 │
│ ├────────────────┤ │ ├───────────────┼──────────────────────────────────────────────────────────────┤
│ │ CVE-2023-3640 │ │ │ │ a per-cpu entry area leak was identified through the │
│ │ │ │ │ │ init_cea_offsets function when... │
│ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2023-3640 │
├────────────────┼────────────────┤ ├───────────────────┼───────────────┼──────────────────────────────────────────────────────────────┤
│ perl-base │ CVE-2023-31484 │ │ 5.36.0-7 │ │ CPAN.pm before 2.35 does not verify TLS certificates when │
│ │ │ │ │ │ downloading distributions over... │
│ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2023-31484 │
└────────────────┴────────────────┴──────────┴───────────────────┴───────────────┴──────────────────────────────────────────────────────────────┘
存在2 个操作系统库,其严重性为 HIGH。这两个库都没有提供可以升级到的版本(参见 Fixed Version 列),以修复我们 Docker 镜像中的漏洞。因此,我们将按以下方式处理它们:
linux-libc-dev:
这是一个运行应用程序时不需要的包。因此,最好还是卸载它!
perl-base
这个操作系统包提供了 Perl 解释器,并且是我们应用程序使用的其他库所必需的。这意味着我们不能卸载它,也不能修复它。因此,我们必须接受风险。接受已知漏洞应由管理层确认和批准。然后,我们可以将漏洞,例如 CVE-2023–31484,添加到 .trivyignore 文件中,再次运行扫描程序。
这里是变更内容:
# Dockerfile
...
FROM base as production
# expose port
EXPOSE 80
# copy the wheel from the build stage
COPY --from=builder /app/dist/*.whl /app/
# install package
RUN pip install /app/*.whl
# copy entrypoint of the app
COPY ["main.py", "./"]
**# Remove linux-libc-dev (CVE-2023-31484)
RUN apt-get remove -y --allow-remove-essential linux-libc-dev**
# command to run
CMD ["uvicorn", "main:app","--host", "0.0.0.0", "--port", "80", "--workers", "1"]
# .trivyignore
# vulnerabilities to be ignored by trivy are added here
CVE-2023-31484
当我们再次运行命令(这次包含 .trivyignore 文件)时:
> trivy image project:latest --scanners vuln --format table --severity CRITICAL,HIGH **--ignorefile .trivyignore**
不再报告严重性为 HIGH 或 CRITICAL 的漏洞:
project:latest (debian 12.0)
Total: 0 (HIGH: 0, CRITICAL: 0)
干杯!
使用 Scikit-Learn 的 SGDRegressor:你需要知道的未授课程
原文:
towardsdatascience.com/sgdregressor-with-scikit-learn-untaught-lessons-you-need-to-know-cf2430439689
通过令人困惑的名称揭示隐藏的算法关系
·发表于 Towards Data Science ·阅读时间 13 分钟·2023 年 3 月 8 日
–
在机器学习领域,线性模型是一种基本技术,广泛用于根据输入数据预测数值。Scikit-learn 中的 SGDRegressor 估算器是一个强大的工具,可以让机器学习从业者快速高效地进行线性回归。
然而,SGDRegressor 的名称对于初学者来说可能有些混淆。本文将解释它的工作原理,并探讨为何这个名称对于刚开始学习机器学习的初学者来说可能会产生误导。
此外,我们将深入探讨 SGDRegressor 中实际上隐藏了多个模型的观点,每个模型都有其特定的参数和超参数。这将引发关于机器学习中模型定义的有趣问题。例如,岭回归或 SVR 是一个独立的模型,还是一个调优过的线性模型?
在你读完这篇文章时,你将更好地理解 SGDRegressor 的内部工作原理,并对线性模型在机器学习中的复杂性以及其实的简单性有新的认识。
1. SGDRegressor 的通常教学内容
SGDRegressor 是 Scikit-Learn 中的一种机器学习算法,它实现了随机梯度下降(SGD)来解决回归问题。由于其处理高维数据集的能力和快速的训练时间,它是大规模回归任务的热门选择。
SGDRegressor
通过使用训练数据的小随机子集而不是整个数据集来迭代地更新模型权重,这使得它在处理大数据集时计算上更高效。它还包括几个可以调整的超参数,以优化性能,包括学习率、惩罚或正则化项和迭代次数。
1.1 线性回归
SGDRegressor
是一个线性模型,使用线性函数来预测目标变量。线性函数的形式为:
y = w[0] + w[1] * x[1] + … + w[p] * x[p]
其中 x[1] 到 x[p] 是输入特征,w[1] 到 w[p] 是线性模型的系数,w[0] 或 b 是截距项。SGDRegressor
算法的目标是找到 w 和 b 的值,使得在预测值和目标变量的实际值之间定义的损失函数最小化。
1.2 随机梯度下降 vs. 梯度下降
SGDRegressor
算法使用随机梯度下降进行优化。随机梯度下降是一种迭代优化算法,它在小批量数据中更新模型参数。该算法使用相对于参数的代价函数的梯度来更新参数。
虽然 SGD 和 GD 都是机器学习中广泛使用的优化算法,但它们的效果取决于具体的问题。SGD 通常对大数据集和非凸问题更快且效果更好,而 GD 对小数据集和凸问题更可靠。
1.3 在 scikit-learn 中使用 SGDRegressor
使用 SGDRegressor
在 scikit-learn 中非常简单。首先,我们需要从 scikit-learn 的 linear_model
模块中导入 SGDRegressor
类。然后,我们可以创建一个 SGDRegressor
类的实例,并将模型拟合到我们的训练数据上。
以下是如何在 scikit-learn 中使用 SGDRegressor
的示例:
from sklearn.linear_model import SGDRegressor
from sklearn.datasets import load_boston
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error
# Load the Boston housing dataset
X, y = load_boston(return_X_y=True)
# Split the dataset into training and test sets
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
# Create an instance of SGDRegressor
sgd_reg = SGDRegressor(max_iter=1000, tol=1e-3, penalty=None, eta0=0.1)
# Fit the model to the training data
sgd_reg.fit(X_train, y_train)
# Make predictions on the test data
y_pred = sgd_reg.predict(X_test)
# Calculate the mean squared error of the predictions
mse = mean_squared_error(y_test, y_pred)
print("Mean squared error:", mse)
在这个示例中,我们加载了波士顿房价数据集,并使用 train_test_split
函数将其拆分为训练集和测试集。然后,我们创建了一个 SGDRegressor
实例,并使用 fit
方法将模型拟合到训练数据上。最后,我们使用 predict
方法对测试数据进行预测,并使用 mean_squared_error
函数计算预测的均方误差。
2. 解开 SGDRegressor 对初学者的困惑名字
在本节中,我们将讨论为什么 SGDRegressor
这个名字对机器学习初学者来说可能会令人困惑。尽管领域内的专家可能熟悉这个名字及其相关算法,但对于新手来说,可能无法立即理解它的功能或工作原理。
然而,我们还将解释,对于专家来说,这个名字不一定是困惑的来源。这是因为他们已经熟悉了该算法及其目的,并且能够很容易地将其与其他类似的算法区分开来。
2.1 为什么它会令人困惑
在我的文章《三步学会机器学习:如何高效学习》中,我解释了如何通过将学习过程分解为三个不同的步骤:模型算法、拟合算法和调优算法,来提高你对机器学习算法的学习。尽管这种方法简单,但在实践中可能难以应用。SGDRegressor 算法就是一个典型的例子。
我认为,选择一个准确反映机器学习算法特征的合适名称对于理解和有效使用该算法至关重要。然而,这并不总是如此,例如混淆的“SGDRegressor”算法。
在 scikit-learn 中,根据机器学习模型是用于回归还是分类任务,有一个惯例是将“regressor”或“classifier”添加到模型名称中。这一惯例在许多示例中都很明显,例如 DecisionTreeClassifier 与 DecisionTreeRegressor,KNeighborsClassifier 与 KNeighborsRegressor 等。
然而,“SGDRegressor”这个名称的问题在于,如果我们将这一命名惯例应用于它,它会暗示“SGD”是一个机器学习模型,而实际上它是一个拟合算法。这对于可能对机器学习算法的不同组件没有清晰理解的初学者来说尤其困惑。
2.2 如何解释这一命名
对于专家来说,“SGDRegressor”的命名惯例可能看起来可以接受。使用“SGD”暗示该模型基于数学函数或参数模型,而非距离或树状模型。因此,“SGD”暗示了尽管 SGD 本身是一个拟合算法,但所使用的隐藏模型。
虽然理论上 SGD 可以用于线性和非线性模型,例如神经网络,但实际上,这个估计器仅实现了线性模型。因此,你会说这个估计器的一个更精确和简洁的名称可能是“LinearSGDRegressor”!是的,没错!但你是否也注意到“SGDRegressor”位于“linear_model”模块中,该模块定义上只实现了线性模型?!
最终,虽然 SGD 指的是具体的拟合算法,但由于此算法仅用于基于数学函数的模型,因此 SGDRegressor 这个名称似乎是合适的。此外,鉴于 SGDRegressor 位于“linear_model”模块中,这表明所使用的模型是线性模型。
3. SGDRegressor 中的隐藏模型
SGDRegressor 中可用的参数让我们能够选择不同的损失函数(squared_error、huber、epsilon_insensitive 或 squared_epsilon_insensitive)和惩罚函数(l1、l2、elasticnet 或 none)的组合。这些组合中的一些对应于传统的统计模型。
有趣的是,历史上,统计学家根据不同的假设和目标开发和构建了不同的模型。相比之下,机器学习提供了一个更统一的框架,其中线性模型保持不变,但损失函数和惩罚可以更改,以实现不同的目标。
3.1 损失函数
损失函数 squared_error、huber、epsilon_insensitive 和 squared_epsilon_insensitive 在对模型及其性能的影响上有所不同。
-
squared_error 损失函数,也称为均方误差(MSE),对较大的错误的惩罚程度比对较小的错误更重,使其对异常值敏感。这个损失函数在线性回归中常用。
-
huber 损失函数比 squared_error 损失函数对异常值的敏感性更低,因为它在较大的残差下从二次误差过渡到线性误差。这使得它在处理有些异常值的数据集时是一个不错的选择。
-
epsilon_insensitive 损失函数用于目标值预计在一定范围内的回归问题。它忽略小于某一阈值(称为 epsilon)的错误,并对较大的错误进行线性惩罚。这个损失函数在支持向量回归中常用。
-
squared_epsilon_insensitive 损失函数类似于 epsilon_insensitive 损失函数,但它通过对较大的错误进行平方惩罚来加重对较大错误的惩罚。当较大的错误需要比线性更多地惩罚时,这个损失函数可以很有用,但较小的错误可以被忽略。
总的来说,损失函数的选择会影响模型处理异常值的能力以及对错误的敏感性。为确保最佳性能,选择适合特定问题的损失函数是非常重要的。
这里是一张总结所有损失函数及其数学表达式的图片。也提供了 Python 实现,允许我们可视化和比较它们的行为。如果你想访问 Python 代码,你可以通过以下链接在 Ko-fi 上支持我:ko-fi.com/s/4cc6555852
。
SGDRegressor 中的损失函数 — 图片由作者提供
需要注意的是,尽管 SGDRegressor 并未明确提供 MAE 损失,但可以通过将 epsilon 不敏感损失函数的超参数 epsilon 设置为零来实现 MAE 损失。这是因为,当 epsilon 为零时,epsilon 不敏感损失的数学表达式等同于 MAE 损失。
最终,尽管损失函数的种类繁多,我们可以观察到它们的构造基于两个基本概念:基本损失是二次的或绝对的,以及引入 epsilon-tube 概念,它创建了两个不同的区域——中央区域和边缘区域——具有不同类型的损失函数。
通过采用这种方法,您可以潜在地创建自己定制的损失函数!
3.2 惩罚项
使用 SGDRegressor 时,我们还可以指定除了所选择的损失函数外还使用的惩罚项。可用的惩罚项包括 L1、L2、Elastic Net 和 None。
L1 惩罚将系数的绝对值添加到损失函数中,从而导致一些系数被设置为零的稀疏模型。该惩罚对特征选择和减少过拟合是有用的。
L2 惩罚将系数的平方添加到损失函数中,从而导致较小但非零的系数。该惩罚还可以帮助减少过拟合并提高模型的泛化能力。
Elastic Net 惩罚结合了 L1 和 L2 惩罚,允许既有稀疏性又有非零系数。它有两个超参数:alpha 控制 L1 和 L2 惩罚之间的权重,l1_ratio 控制 L1 和 L2 惩罚之间的平衡。
最后,None 意味着不使用惩罚,模型仅用所选择的损失函数进行拟合。
选择合适的惩罚项取决于具体问题和数据的性质。一般来说,L1 和 Elastic Net 惩罚有助于特征选择和稀疏模型,而 L2 惩罚有助于泛化和避免过拟合。
这里是来自scikit learn 文档的有趣可视化
来自 scikit learn 的惩罚项
3.3 隐藏模型的展示
SGDRegressor 提供了在指定不同组合的损失和惩罚参数方面的灵活性。在 SGDRegressor 中,某些损失和惩罚参数的组合对应于具有特定名称的知名模型。以下图像提供了这些选项的概述,包括相应的模型名称,我们将详细探讨其中的一些。
SGDRegressor 中的损失函数和惩罚项及其对应模型名称 — 图像由作者提供
平方误差
在 SGDRegressor 中,最简单的组合是 squared_error 无惩罚,这对应于 Scikit-learn 的 LinearRegression 估计器中的经典线性回归模型。然而,这种命名方式可能会产生误导。虽然所有这些模型都是回归模型且为线性模型,但将它们称为“线性回归”可能会造成混淆。为了避免这种情况,最好使用“线性模型”一词,而将“线性回归”一词专门保留用于 OLS 回归。
Lasso 是一种具有 L1 正则化的线性 (OLS) 回归模型,鼓励系数估计的稀疏性。
另一方面,岭回归在线性 (OLS) 回归模型中添加了 L2 惩罚,以帮助减轻多重共线性的影响。
ElasticNet 结合了 L1 和 L2 正则化的惩罚,以在 Lasso 的稀疏性和 Ridge 的稳定性之间取得平衡。
使用平方误差时,我们可以轻松识别每个惩罚对应的具体模型名称。然而,对于其他损失函数,并不总是有特定的名称与每个惩罚关联。在我看来,研究人员历史上专注于寻找正则化或线性回归系数的惩罚效果,导致每种惩罚类型都有特定名称。对于其他损失函数,添加惩罚项似乎不再新颖。
Huber 损失
Huber 损失函数是一种对离群值不那么敏感的替代损失函数,适用于具有显著离群值的数据集。Huber 损失函数是平方损失函数和绝对损失函数的组合。对于小误差,它的行为像平方损失函数,而对于大误差,它的行为像绝对损失函数。这使得它比平方损失函数对离群值更具鲁棒性,同时对小误差仍提供良好的性能。
Epsilon 不敏感损失
Epsilon-不敏感损失是另一种常用于线性模型的损失函数。epsilon 参数用于定义在此范围内的误差被视为零。此损失函数对具有噪声输出变量的数据集非常有用,因为它可以帮助减少输出变量中小波动的影响。
实际上,Epsilon 不敏感损失和 L2 正则化的组合也被称为 SVR(支持向量回归)。使用 epsilon 不敏感管道概念使得添加惩罚成为强制性,因为没有它可能会有无限多的解决方案。术语“支持向量”之所以被使用,是因为正如 L1 正则化项对系数会导致某些系数变为零一样,应用于数据集的 L1 损失(绝对损失)将导致某些数据点不用于计算系数,只留下其余的数据点,这些数据点被称为支持向量。
值得注意的是,Huber 回归通常被描绘为不那么敏感,但它与 SVR 共享这一特性,因为两者都对较大的值使用绝对值。虽然“epsilon 不敏感”一词强调误差为零的中心区域,但较大值的绝对误差对最终模型也可以有显著影响。
要了解有关 SVR 和 Epsilon 不敏感损失及其在 Scikit-learn 中的应用,可以阅读这篇文章:使用 Scikit-learn 理解 SVR 和 Epsilon 不敏感损失
平方 epsilon 不敏感损失
我们可能以前从未听说过这个:平方 epsilon 不敏感损失! 正如其名,它基于epsilon 不敏感损失,但使用的是平方误差而非绝对误差。问题是,为什么使用这个特定的损失函数?嗯,为什么不呢。
答案是,在机器学习中,“没有免费的午餐”理论表明,一个模型不能在所有数据集上表现良好,因此需要测试不同的损失函数。在某些情况下,平方 epsilon 不敏感损失可能是最佳选择。
3.4 一个可调模型还是不同模型?
为了理解这些模型的不同名称,我们可以从两个不同的角度来分析:统计学角度和机器学习角度。
在统计学领域,多年来已经开发出各种模型来解决不同类型的问题。因此,存在许多具有不同假设、约束和特性的模型。另一方面,机器学习框架相对简单,因为它围绕线性模型展开,可以通过更改损失函数和应用惩罚来避免过拟合,从而轻松修改。
这种简单性和灵活性使得机器学习从业者更容易进行实验并适应不同的问题设置,从而开发出适用于各种应用的新型有效模型。
在我之前的文章“三步学习机器学习:如何高效学习它”中,我强调了区分算法的三个部分——模型算法、模型拟合算法和模型调优算法的重要性。这种方法有助于简化对机器学习算法的理解。
至于 SGDRegressor,以下是三个步骤:
-
模型:我们可以将 LASSO、Ridge、弹性网、SVM 和 Huber 回归视为一个整体模型,即线性模型表示为 y = wX + b。
-
拟合:使用的拟合算法是随机梯度下降(SGD)。
-
调优:可以调节的超参数包括损失和惩罚等。
尽管 sci-kit learn 在同一个 linear_model 模块中有多个独立的模型如 LinearRegression、LASSO 和 Ridge,但是否这些模型实际上是同一个模型并不重要。重点应放在理解它们的内部功能上,因为名字可能会产生误导。
在总结本节之前,我想到一个问题:估计器 LinearRegression 是否真的属于机器学习模型,因为它不可调且没有任何需要调整的超参数?
结论和主要要点
总之,scikit-learn 中的 SGDRegressor 提供了一个灵活且强大的线性回归工具。它的多种损失函数和惩罚选项为用户提供了许多自定义模型以满足特定需求的选择。此外,使用 SGD 拟合非凸函数的能力相较于标准梯度下降是一个显著的优势。需要注意的是,损失和惩罚参数应被视为需要调整的超参数。通过应用模型、拟合和调整的学习框架,数据科学家可以利用 SGDRegressor 在他们的线性回归任务中实现最佳结果。
SHAP:在 Python 中解释任何机器学习模型
原文:
towardsdatascience.com/shap-explain-any-machine-learning-model-in-python-72f0bea35f7c
照片由 Priscilla Du Preez 提供,来源于 Unsplash
您的 SHAP、TreeSHAP 和 DeepSHAP 综合指南
·发表于 Towards Data Science ·阅读时间 13 分钟·2023 年 1 月 11 日
–
动机
故事时间!
想象一下你训练了一个机器学习模型来预测抵押贷款申请者的违约风险。一切都很好,性能也很出色。但模型是如何工作的?模型是如何得出预测值的?
我们站在那里说模型考虑了几个变量,而这些多维关系和模式复杂到用简单的语言无法解释。
这就是模型可解释性可以拯救局面的地方。在可以剖析机器学习模型的算法中,SHAP 是该领域中较为中立的算法之一。在这篇博客中,我们将深入探讨以下内容:
-
什么是 Shapley 值?
-
如何计算 Shapley 值?
-
如何在 Python 中使用它?
-
SHAP 如何支持局部和全局可解释性?
-
SHAP 库中有哪些可用的可视化?
-
SHAP 的常见变体如何工作?— TreeSHAP 和 DeepSHAP
-
LIME 与 SHAP 相比如何?
Shapley 值
让我们玩个游戏
当一支由十一名球员组成的球队赢得世界杯时,谁是最有价值的球员?Shapley 值是一种分解算法,客观地将最终结果分配给一组因素。在解释机器学习模型时,Shapley 值可以理解为单个输入特征对模型预测值的贡献程度。
快速示例 — Shapley 值是如何工作的?
为了简单起见,假设我们有三名进攻球员,每名球员有不同的预期进球数。我们还知道这三名球员并不总是相互配合良好,这意味着根据这三名球员的组合,预期进球数可能会有所不同:
作者提供的图片
作为基准,我们不使用这三名球员,即特征数 f = 0,团队的预期进球数将是 0.5。每一个箭头向下的矩阵表示包含一个新特征(或在我们情况下是一个新球员)时可能的逐步增量。
遵循逐步扩展玩家集的思路,这意味着我们可以计算每一个箭头的边际变化。例如,当我们从不使用任何玩家(用空集符号 ∅ 表示)移动到仅使用玩家 1 时,边际变化是:
作者提供的图片
要获得玩家 1 在所有三名玩家中的总体贡献,我们需要对每一个可能出现玩家 1 边际贡献的情景重复相同的计算:
作者提供的图片
通过所有边际变化,我们可以使用以下公式计算它们的权重:
作者提供的图片
或者,简单来说:这只是指向同一行的所有边的数量的倒数。这意味着:
作者提供的图片
有了这些,我们现在可以计算玩家 1 的 SHAP 值,以获得预期进球数:
作者提供的图片
对另外两名玩家进行相同的操作,我们将得到:
-
玩家 1 的 SHAP = -0.1133
-
玩家 2 的 SHAP = -0.0233
-
玩家 3 的 SHAP = +0.4666
如果我是主教练,我在这种情况下只会使用玩家 3。
这与另一种操作符 Choquet Integral 非常相似,对于那些数学更精通的朋友。
计算复杂度
以上述仅有 3 个特征的例子为例,我们需要考虑 8 个不同的模型,每个模型有不同的输入特征集,以全面解释所有特征。事实上,对于一个完整的N特征集,总子集的数量将是2^N。因此,在使用 SHAP 解释训练有大量且更重要的是宽数据集的机器学习模型时,我们需要注意预期的运行时间。
在接下来的章节中,我们将首先深入探讨如何在 Python 中使用 SHAP,然后将大部分注意力转向 SHAP 的不同变体,这些变体旨在通过近似技术或针对模型拓扑特定的技术来应对 SHAP 的复杂性。
Pascal 三角形 — 图片来源于 维基百科
Python 中的 SHAP
接下来,让我们探讨如何在 Python 中使用 SHAP。
SHAP (SHapley Additive exPlanations) 是一个兼容大多数机器学习模型拓扑的 Python 库。安装非常简单,只需 pip install shap
。
SHAP 提供了两种解释机器学习模型的方法——全局解释和本地解释。
使用 SHAP 进行本地可解释性
本地可解释性试图解释特定预测背后的驱动因素。在 SHAP 中,个体 Shapley 值就是用来做这个的,如早期部分的快速示例所示。
在 SHAP 的工具集中,有两种可视化方法用于解释个体预测:瀑布图和力图。瀑布图让你更好地理解逐步推导预测结果的过程,而力图旨在提供特征对预测结果偏差的相对贡献强度。
注意: 两种可视化都包括了一个整体期望预测值(或基准值)。这可以理解为训练集上模型输出的平均值。
瀑布图
# Code snippet from SHAP github page
import xgboost
import shap
# train an XGBoost model
X, y = shap.datasets.boston()
model = xgboost.XGBRegressor().fit(X, y)
# explain the model's predictions using SHAP
# (same syntax works for LightGBM, CatBoost, scikit-learn, transformers, Spark, etc.)
explainer = shap.Explainer(model)
shap_values = explainer(X)
# visualize the first prediction's explanation
shap.plots.waterfall(shap_values[0])
图片来自 SHAP GitHub 页面(MIT 许可证)
-
在 y 轴上,你可以找到特征的名称和值。
-
在 x 轴上,你可以找到基准值
E[f(X)] = 22.533
,这表示训练集上的平均预测值。 -
图中红色条形表示特征对预测值的正贡献。
-
图中蓝色条形表示特征对预测值的负贡献。
-
条形上的标签表示归因于参数的模型基准预测值的偏差。例如,AGE = 65.2 对预测值的偏差从基准值 22.533 上贡献了 +0.19。
-
条形按其对预测值的绝对重要性降序排列。
力图
# Code snippet from SHAP github page
# visualize the first prediction's explanation with a force plot
shap.plots.force(shap_values[0])
图片来自 SHAP GitHub 页面(MIT 许可证)
-
在 x 轴上,你可以找到基准值。这表示训练集上平均预测值的大致位置。
-
在 x 轴上,你还可以找到用粗体数字标记的模型输出。这表示该记录的预测值。
-
在图表底部,你可以找到特征的名称和值,标记为红色或蓝色。
-
所有在模型输出左侧的红色条形是对预测偏离基准值有正面贡献的特征。特征的名称在条形的底部。条形的长度表示特征的贡献。
-
模型输出右侧的所有蓝色条形图表示对预测偏离基准值产生负面贡献的特征。特征的名称位于条形图底部。条形图的长度表示特征的贡献。
SHAP 的全球可解释性
全球可解释性可以理解为在整个数据集中理解每个特征的整体重要性,并提供对数据和潜在模式的一般了解。由于分解个体预测贡献和在数据中聚合的模糊性,尝试全球可解释性的方法不止一种。示例包括信息增益、汇总权重、基于置换的特征重要性和 Shapley 值。SHAP 当然专注于最后一个。
SHAP 提供了一种可视化方法,我们可以查看特征在数据集中的平均 Shapley 值。与其他使用统计上更复杂解释来提供重要性度量的机制不同,SHAP 的全球可解释性通过让你能够说,平均而言,特征关系使得“Class 1”数据记录的预测值比“Class 0”数据记录高约 1.0,从而提供了一个立即可理解的影响。
图像来自SHAP GitHub 页面(MIT 许可证)
SHAP 的全球可解释性功能允许我们排查或调查模型偏差。以上面的图像为例,年龄通常是一个非常重要的特征。这是否可能表明模型对特定年龄组存在不必要的偏见?此外,一个非常重要的特征是否可能是潜在的数据泄露?所有这些问题都使我们在部署更负责任且强健的机器学习模型之前,能够改进模型。
注意: 如果你有兴趣了解更多关于负责任的人工智能,我还写了一篇关于如何通过 5 个简单步骤来实现这一目标的文章。
负责任的人工智能系统的 5 个步骤
SHAP 支持的另一种可视化是局部可解释性部分的力图堆叠版本。通过堆叠力图,我们可以可视化模型与不同输入值的特征之间的交互。这为我们提供了基于 Shapley 值的聚类视图,并提供了模型如何看待数据的视角。这对修正和验证假设以及基础业务逻辑非常有用。在分析所有 Shapley 值后,你可能还会发现数据分割的新方法!
# Code snippet from SHAP github page
# visualize all the training set predictions
shap.plots.force(shap_values)
图片来源于SHAP GitHub 页面(MIT 许可证)
SHAP 的变体
TreeSHAP
-
优点: 高效且准确的算法,用于计算基于树模型的 Shapley 值。
-
缺点: 仅适用于基于树的模型。
与原始 SHAP 不同,TreeSHAP 是特定于基于树的机器学习模型的。这意味着 TreeSHAP 仅适用于决策树、随机森林、梯度提升机等模型。
TreeSHAP 特定于树模型,因为它利用树结构来更高效地计算准确的 Shapley 值。由于这些结构在其他模型拓扑中不存在,因此 TreeSHAP 仅限于基于树的模型。
TreeSHAP 可以通过干预和树路径依赖的方法计算 Shapley 值。这可以在feature_perturbation
参数中指定。树路径依赖方法递归地计算条件期望的变化。我们以一个接受 2 个特征**(x, y)**的简单决策树为例:
示例决策树 — 作者提供的图片
在上面的示例中,我们有一个包含 7 个节点的决策树,接受两个特征**(x, y)来预测z**,并且已经用8个训练样本进行了训练。为了计算在联盟**(x=10, y=5)中y对z预测的局部**贡献,我们需要考虑以下因素:
-
对于**(x=10, y=5),模型将从节点 1 移动到节点 3 并到达节点 6。由于节点 6 是叶节点,模型确定预测为z=4**。
-
对于**(x=10),模型将从节点 1 移动到节点 3。然而,由于节点 3 不是叶节点,预测值可以推断为节点 3 所有叶节点的加权和。在通过节点 3 的 5 个训练样本中,有两个预测为z=4**,而其他的预测为z=24。加权和为4*(2/5) + 24*(3/5)=1.6 + 14.4 = 16。
-
在联盟**(x=10, y=5)中,y对z预测的边际贡献可以计算为Prediction(x=10, y=5) — Prediction(x=10) = 4–16= -12**。
注意: 这里的负贡献并不意味着特征y不重要,而是特征y将预测值推高了**-12**。
通过对所有特征继续这一过程,TreeSHAP 将获得所有 Shapley 值,并提供局部可解释性(使用上述方法)和全局可解释性(对训练集中的所有局部可解释性结果进行平均)
顾名思义,干预方法通过人为调整感兴趣特征的值来计算 Shapley 值。在我们上述的例子中,这可能是将y从 5 改为 4。为了估计敏感性,TreeSHAP 需要反复使用背景集/训练集作为参考点(当我们在最后一节讨论 LIME 时会再次提到),其线性运行时间复杂度。因此,在使用干预方法时,我们应更加关注 TreeSHAP 的可扩展性。
import shap
# Load the data
X_train, y_train, X_test, y_test = load_data()
# Train the model
model = MyModel.train(X_train, y_train)
# Explain the model's predictions using the tree path dependent approach
explainer = shap.TreeExplainer(
model,
X_train,
feature_perturbation='tree_path_dependent')
shap_values_path = explainer.shap_values(X_test)
# Display the explanations
shap.summary_plot(shap_values_path, X_test)
# Explain the model's predictions using the interventional approach
explainer = shap.TreeExplainer(
model,
X_train,
feature_perturbation='interventional')
shap_values_interv = explainer.shap_values(X_test)
# Display the explanations
shap.summary_plot(shap_values_interv, X_test)
DeepSHAP
-
优点: 高效的算法,用于近似深度学习或基于神经网络的模型的 Shapley 值。兼容 Tensorflow 和 PyTorch
-
缺点: 仅适用于深度学习或基于神经网络的模型。由于算法的近似特性,比 SHAP 的准确性低。
讨论可解释性时,我们不能忽视神经网络。DeepSHAP 是 SHAP 和 DeepLIFT 的结合,旨在揭示深度学习模型背后的哲学。它专为深度学习模型设计,这使得 DeepSHAP 仅适用于基于神经网络的模型。
DeepSHAP 试图近似 Shapley 值。解释 DeepSHAP 的一种相对原始的方法是,它试图通过使用梯度或偏导数来分配特征x的局部边际贡献,前提是使用一个有意义的背景/参考点(例如,图像识别模型的全黑背景,预测暴富机会的 0%)。
注意:有进一步研究发布了 DeepSHAP 的通用版本——G-DeepSHAP。你可以在arxiv阅读。
import shap
# Load the data
X_train, y_train, X_test, y_test = load_data()
# Train the model
model = MyModel.train(X_train, y_train)
# Explain the model's predictions using TreeSHAP
explainer = shap.DeepExplainer(model, X_train)
shap_values = explainer.shap_values(X_test)
# Display the explanations
shap.summary_plot(shap_values, X_test)
LIME — SHAP 的替代方法
LIME(局部可解释模型无关解释)是解释预测的 SHAP 的替代方法。它是一种模型无关的方法,默认假设内核大小(解释个体预测时考虑的局部邻域的大小)来近似特征对局部实例的贡献。一般来说,选择较小的内核大小时,LIME 提供的结果将更倾向于局部解释特征值对预测的贡献。(即,较大的内核大小往往提供更全局的视角)
然而,内核大小的选择应根据数据和模式仔细决定。因此,在使用 LIME 时,我们应考虑相应地调整内核大小,以获得对机器学习模型的合理解释。
要尝试一下,我们可以安装并使用该软件包:
pip install lime
import lime
import lime.lime_tabular
# Load the data
X_train, y_train, X_test, y_test = load_data()
feature_names = X_train.columns
# Train the model
model = MyModel.train(X_train, y_train)
# Explain the model's predictions using LIME
explainer = lime.lime_tabular.LimeTabularExplainer(
X_train, feature_names=feature_names)
# Choose a kernel size for the local neighborhood
kernel_size = 10
# Explain the model's prediction for a single instance
instance = X_test[0]
exp = explainer.explain_instance(
instance,
model.predict,
num_features=10,
kernel_size=kernel_size)
# Display the explanations
exp.show_in_notebook(show_all=False)
结论
最后总结一下,这里是对本文讨论内容的简要总结:
-
SHAP 是一种基于博弈论的方法,用于解释机器学习模型。
-
SHAP 考虑所有可能的特征组合以评估每个特征的影响。
-
特征 f 对于本地预测实例的 SHAP 值是由于特征的引入在包含 f 的所有可能特征组合中的边际变化的加权总和。
-
边际变化的权重根据 f × C(F, f) 的倒数进行,其中 F 是实际模型考虑的特征数量,而 f 是计算边际变化时考虑的特征数量。
-
由于 SHAP 考虑了所有可能的特征组合,因此算法不会线性扩展,会受到维度灾难的影响。
-
为了应对 SHAP 的计算复杂性,已经常用几种 SHAP 的变体:
图片来源于作者
-
我们应该考虑对基于树的模型使用 TreeSHAP,对基于深度学习的模型使用 DeepSHAP。
-
LIME 是一种替代 SHAP 的模型无关方法,用于近似特征的贡献。
-
LIME 的解释可以根据内核大小的选择显著不同。
关于 SHAP 的这次全面介绍就是这些了。我希望你发现这些内容对提升你的写作水平或开始写作有所帮助。如果你喜欢这篇文章,你也可以通过下面的我的附属链接订阅 Medium 来支持我。这是一个我发现了很多有趣读物的平台。即使你完全不打算订阅,你也可以通过点“赞”来支持我和我的创作。
[## 通过我的推荐链接加入 Medium — Louis Chan
阅读 Louis Chan 的每一个故事(以及 Medium 上成千上万的其他作家的故事)。你的会员费直接支持…
louis-chan.medium.com](https://louis-chan.medium.com/membership?source=post_page-----72f0bea35f7c--------------------------------)
最后但绝对不是最不重要的,如果我遗漏或误解了任何关键内容,请随时在评论中指出或通过 LinkedIn 给我发消息。让我们一起保持知识的流动,共同在这个领域中进步!
[## Louis Chan — 主任级 GCP 数据与 ML 工程师 — 副总监 — KPMG 英国 | LinkedIn
有抱负、好奇且富有创意的个人,坚信知识领域之间的相互联系。
www.linkedin.com](https://www.linkedin.com/in/louis-chan-b55b9287?source=post_page-----72f0bea35f7c--------------------------------)
参考文献
-
Lundberg, Scott M., 和 Su-In Lee. “统一的模型预测解释方法。” 神经信息处理系统进展,2017。
-
Lundberg, Scott, 和 Su-In Lee. “一致的个性化特征归因用于树集成。” arXiv 预印本 arXiv:1802.03888, 2018.
-
Ribeiro, Marco Tulio, Sameer Singh, 和 Carlos Guestrin. “我为什么应该相信你?解释任何分类器的预测。” 第 22 届 ACM SIGKDD 国际知识发现与数据挖掘大会论文集, 2016.
-
Ribeiro, Marco Tulio, Sameer Singh, 和 Carlos Guestrin. “Anchors: 高精度模型无关解释。” arXiv 预印本 arXiv:1802.07814, 2018.
二元和多类目标变量的 SHAP
原文:
towardsdatascience.com/shap-for-binary-and-multiclass-target-variables-ff2f43de0cf4
提供一个指南,讲解当模型预测分类目标变量时如何编写代码和解读 SHAP 图
·发表于 Towards Data Science ·9 分钟阅读·2023 年 9 月 4 日
–
照片由 Nika Benedictova 提供,来源于 Unsplash
SHAP 值展示了模型特征对预测的贡献。这在我们使用 SHAP 进行分类时也同样适用。不同的是,对于二元目标变量,我们用对数几率来解释这些值。对于多类目标,我们使用softmax。我们将:
-
更深入地讨论这些解释
-
提供用于显示分类问题的 SHAP 图的代码
-
探索聚合 SHAP 值的新方法以适应多类目标
你还可以观看关于该主题的视频:
之前的 SHAP 教程
我们继续之前的 SHAP 教程。它深入探讨了连续目标变量的 SHAP 图。你将发现这些图及其见解对于分类目标变量也是类似的。你还可以在 GitHub 上找到完整的项目。
如何创建和解读 SHAP 图:瀑布图、力图、均值 SHAP、蜜蜂散点图和依赖图
towardsdatascience.com
总结一下,我们使用 SHAP 解释了基于海螺数据集构建的模型。该数据集包含4,177个实例,下面可以看到特征的示例。我们使用8个特征来预测 y——海螺壳上的环数。环数与海螺的年龄有关。在本教程中,我们将 y 分成不同的组,以创建二元和多类目标变量。
X 特征矩阵(来源:UCI 机器学习库)(许可证:CC0:公共领域)
二元目标变量
对于连续目标变量,我们发现每个实例都有 8 个 SHAP 值——每个模型特征一个。如图 1所示,如果我们将这些值与平均预测值E[f(x)]相加,我们就得到了该实例的预测值f(x)。对于二元目标变量,我们也有相同的性质。区别在于我们将值解释为正预测的对数几率。
图 1:根据对数几率解释 SHAP 值(来源:作者)
为了理解这一点,让我们深入研究 SHAP 图。我们从创建一个二元目标变量(第 2 行)开始。我们基于 y 创建了两个组:
-
1 如果海螺的环数高于平均水平
-
0 否则
#Binary target varibale
y_bin = [1 if y_>10 else 0 for y_ in y]
我们使用这个目标变量和 8 个特征来训练一个XGBoost 分类器(第 2-3 行)。该模型的准确率为96.6%。
#Train model
model_bin = xgb.XGBClassifier(objective="binary:logistic")
model_bin.fit(X, y_bin)
我们现在计算 SHAP 值(第 2-3 行)。我们输出这个对象的形状(第 5 行),得到**(4177, 8)**。因此,与连续目标变量一样,我们每个预测和特征都有一个 SHAP 值。稍后,我们将看到这对于多类目标变量是如何不同的。
#Get shap values
explainer = shap.Explainer(model_bin)
shap_values_bin = explainer(X)
print(shap_values_bin.shape) #output: (4177, 8)
我们为第一个实例显示了一个瀑布图(第 6 行)。我们可以在图 2中看到结果。注意,代码与连续变量的代码相同。除了数字外,瀑布图也看起来类似。
# waterfall plot for first instance
shap.plots.waterfall(shap_values_bin[0])
现在E[f(x)] = -0.789 给出了所有 4,177 个海螺的平均预测对数几率。这是正预测(1)的对数几率。对于这个特定的海螺,模型预测其有0.3958的概率具有高于平均水平的环数(即P = 0.3958)。这给出了预测的对数几率f(x) = ln(0.3958/(1–0.3958)) = -0.423。
图 2:带有二元目标变量的瀑布图(来源:作者)
因此,SHAP 值表示预测对数几率与平均预测对数几率之间的差异。正的 SHAP 值增加对数几率。例如,剥壳重量增加了1.32的对数几率。换句话说,这个特征增加了模型预测环数高于平均水平的概率。类似地,负值则减少对数几率。
我们也可以以之前相同的方式聚合这些值。好消息是,像蜜蜂散点图或均值 SHAP 这样的图形解释将保持不变。只需记住,我们处理的是对数赔率。现在让我们看看这种解释如何在多类别目标变量中发生变化。
多类别目标变量
我们通过创建一个新的目标变量 (y_cat) 来开始,该变量有 3 个类别——年轻(0)、中等(1)和老(2)。如前所述,我们训练了一个 XGBoost 分类器来预测这个目标变量(第 5–6 行)。
#Categorical target varibale
y_cat = [2 if y_>12 else 1 if y_>8 else 0 for y_ in y]
#Train model
model_cat = xgb.XGBClassifier(objective="binary:logistic")
model_cat.fit(X, y_cat)
对于这个模型,我们不能再谈论“正预测”。如果我们输出第一个实例的预测概率(第 2 行),我们可以看到这一点。这给我们 [0.2562, 0.1571, 0.5866]。在这种情况下,第三个概率最高,因此海洋蜗牛被预测为老(2)。这对 SHAP 意味着我们不能再只考虑正类的值。
# get probability predictions
model_cat.predict_proba(X)[0]
当我们计算 SHAP 值时可以看到这一点(第 2–3 行)。代码与二分类模型相同。然而,当我们输出形状(第 5 行)时,我们得到 (4177, 8, 3)。现在我们为每个实例、特征和类别都有一个 SHAP 值。
#Get shap values
explainer = shap.Explainer(model_cat)
shap_values_cat= explainer(X)
print(np.shape(shap_values_cat))
因此,我们必须在单独的瀑布图中显示每个类别的 SHAP 值。我们在下面的代码中为第一个实例执行此操作。
# waterfall plot for class 0
shap.plots.waterfall(shap_values_cat[0,:,0])
# waterfall plot for class 1
shap.plots.waterfall(shap_values_cat[0,:,1])
# waterfall plot for class 2
shap.plots.waterfall(shap_values_cat[0,:,2])
图 3 给出了类别 0 的瀑布图。该图解释了每个特征如何对模型预测为此类别做出贡献。也就是说,与该类别的平均预测相比。我们看到该类别的概率相对较低(即 0.2562)。我们可以看到,去壳重量特征对这一低概率做出了最显著的贡献。
图 3:类别 0 的瀑布图(来源:作者)
图 4 给出了其他类别的输出。你会注意到 f(x) = 1.211 在类别 2 中是最大的。这是有道理的,因为我们看到这个类别的概率也是最大的(0.5866)。在分析该实例的 SHAP 值时,可能要重点关注这个瀑布图。这是该海洋蜗牛的类别预测。
图 4:类别 1 和 2 的瀑布图(来源:作者)
使用 Softmax 解释值
由于我们现在处理的是多个类别,f(x) 是以 softmax 形式给出的。我们可以使用下面的函数将 softmax 值转换为概率。fx 给出了上述瀑布图中的三个 f(x) 值。结果是 [0.2562, 0.1571, 0.5866]。这就是我们看到的实例 0 的预测概率!
def softmax(x):
"""Compute softmax values for each sets of scores in x"""
e_x = np.exp(x - np.max(x))
return e_x / e_x.sum(axis=0)
# convert softmax to probability
fx = [0.383,-0.106,1.211]
softmax(fx)
聚合多类别 SHAP 值
这些 SHAP 值可以使用任何 SHAP 图进行汇总。然而,像瀑布图一样,每个类别都会有单独的图。分析这些可能会很繁琐,尤其是当目标变量中有很多类别时。因此,我们将讨论一些其他的汇总方法。
首先是平均 SHAP 图的一个版本。我们分别计算每个类别的 SHAP 值的绝对平均值(第 2–4 行)。然后我们创建一个条形图,每个类别和特征都有一个条形。
# calculate mean SHAP values for each class
mean_0 = np.mean(np.abs(shap_values_cat.values[:,:,0]),axis=0)
mean_1 = np.mean(np.abs(shap_values_cat.values[:,:,1]),axis=0)
mean_2 = np.mean(np.abs(shap_values_cat.values[:,:,2]),axis=0)
df = pd.DataFrame({'small':mean_0,'medium':mean_1,'large':mean_2})
# plot mean SHAP values
fig,ax = plt.subplots(1,1,figsize=(20,10))
df.plot.bar(ax=ax)
ax.set_ylabel('Mean SHAP',size = 30)
ax.set_xticklabels(X.columns,rotation=45,size=20)
ax.legend(fontsize=30)
我们可以在图 5中看到输出。有一点需要提到的是,每个条形图显示的是所有预测的平均值。然而,实际的预测类别在每种情况下都会有所不同。因此,您可能会因为 SHAP 值不能解释预测类别而使均值产生偏差。这可能是我们看到中等类别均值较小的原因。
图 5:多分类目标变量中每个类别的平均 SHAP 值(来源:作者)
为了避免这个问题,我们可以集中在预测类别的 SHAP 值上。我们首先获取每个实例的预测类别(第 2 行)。我们创建一组新的 SHAP 值(new_shap_values)。这是通过遍历原始值并仅选择与该实例预测相对应的那一组值来完成的(第 5–7 行)。
# get model predictions
preds = model_cat.predict(X)
new_shap_values = []
for i, pred in enumerate(preds):
# get shap values for predicted class
new_shap_values.append(shap_values_cat.values[i][:,pred])
然后我们将原始对象中的 SHAP 值替换掉(第 2 行)。现在,如果我们输出形状,得到的是(4177, 8)。换句话说,我们回到了每个实例一组 SHAP 值的情况。
# replace shap values
shap_values_cat.values = np.array(new_shap_values)
print(shap_values_cat.shape)
这种方法的一个好处是可以轻松使用内置的 SHAP 图。例如,图 6中的平均 SHAP 图。我们可以将这些值解读为特征对预测类别的平均贡献。
shap.plots.bar(shap_values_cat)
图 6:多分类目标变量中预测类别的平均 SHAP 值(来源:作者)
我们也可以使用 beeswarm 图。然而,注意到我们没有看到 SHAP 值与特征值之间的明确关系。这是因为特征的关系会根据预测类别而有所不同。年长的鲍鱼体型会更大。例如,大的壳重会导致老年(2)预测的概率更高。年轻(0)预测则相反。
shap.plots.beeswarm(shap_values_cat)
图 6:多分类目标变量的 beeswarm 图(来源:作者)
希望现在清楚如何解读二分类和多分类目标变量的 SHAP 值。然而,您可能会想知道为什么它们以对数几率和 softmax 的形式给出。将它们解释为概率可能更有意义。
这源于 SHAP 值的计算方式。即同时通过线性模型进行计算。如果我们需要用线性模型预测一个二元或多类变量,我们会分别使用逻辑回归或 Softmax 回归。这些连接函数是可微的,并允许我们将模型预测公式化为参数和特征的线性方程。同样,这些特性用于高效地估计 SHAP 值。
了解更多关于 SHAP 的信息:
SHAP 版本 0.42.1 中的图可以告诉你关于你的模型的哪些信息
[towardsdatascience.com ## SHAP 的局限性
SHAP 如何受到特征依赖性、因果推断和人为偏差的影响
[towardsdatascience.com ## 使用 SHAP 调试 PyTorch 图像回归模型
使用 DeepShap 来理解和改进支持自动驾驶汽车的模型
[towardsdatascience.com
我希望你喜欢这篇文章!你可以通过成为我的 推荐会员 来支持我 😃
[## 通过我的推荐链接加入 Medium — Conor O’Sullivan
作为 Medium 会员,你的会员费用的一部分将分配给你阅读的作者,并且你可以完全访问每个故事……
| Twitter | YouTube | Newsletter — 免费注册获取 Python SHAP 课程
参考文献
Stackoverflow 如何解释多类分类问题的 base_value,当使用 SHAP 时?stackoverflow.com/questions/65029216/how-to-interpret-base-value-of-multi-class-classification-problem-when-using-sha/65034362#65034362
SHAP 用于时间序列事件检测
图片由 Luke Chesser 提供,发布在 Unsplash
使用修改版的 KernelSHAP 进行时间序列事件检测
·
关注 发表在 Towards Data Science ·8 min 阅读 ·2023 年 2 月 1 日
–
特征重要性是一种广泛使用的技术,用于解释机器学习模型如何做出预测。该技术为每个特征分配一个分数或权重,指示该特征对预测的贡献程度。这些分数可以用来识别最重要的特征,并了解模型如何做出预测。其中一种常用的版本是 Shapley 值,这是一种基于博弈论的模型无关度量,将“支付”(预测)公平地分配给特征[1]。Shapley 值的一种扩展是 KernelSHAP,它使用核技巧以及局部替代模型来近似 Shapley 值,这使得它能够计算更复杂模型(如神经网络)的特征重要性值[2]。
KernelSHAP 通常用于解释时间序列预测,但在这个领域确实存在一些显著的约束和缺陷:
-
时间序列预测通常涉及大量过去数据,这可能会导致在应用 KernelSHAP 时出现数值下溢错误,尤其是在多变量时间序列预测中[3]。
-
KernelSHAP 假设特征独立性。这在表格数据情况下通常有效,但在时间序列中,特征和时间步的独立性往往是一种例外,而非常态[3]。
-
KernelSHAP 使用已拟合于数据扰动的线性模型的系数。然而,在时间序列的情况下,向量自回归模型(VAR)通常比仅使用线性模型[3]更适合建模过程。
为了解决这些问题,J.P. 摩根的 AI 研究部门(Villani 等人)在他们的 2022 年 10 月论文 [3]中提出了更适合时间序列数据的 KernelSHAP 变体:
-
研究人员首先创建了 VARSHAP,这是一种使用 VAR 模型而非线性模型的 KernelSHAP 修改版。这种修改使得研究人员还计算了一种封闭形式的方法来计算 AR、MA、ARMA 和 VARMAX 模型的 SHAP 值。
-
基于 VARSHAP,研究人员提出了时间一致性 SHAP,利用问题的时间成分来减少 SHAP 值的计算量。
使用时间一致性 SHAP 度量,研究人员展示了一种通过捕捉特征重要性的激增来进行事件检测的有前途的方法。
在这篇文章中,我将首先解释 KernelSHAP 的计算方法以及如何将其修改为 VARSHAP。然后还将解释如何获取时间一致性 SHAP(TC-SHAP)以及如何使用 TC-SHAP 来识别时间序列分析中的事件。
KernelSHAP 和 VARMAX
SHAP 值的公式如[2]所提供的:
方程 1:SHAP 方程
上述方程中的 phi 是特征 i 的 SHAP 值,给定值函数 v(值函数通常是模型预测)。C* 是所有特征的集合,N 是 C 的大小或特征数量。P© 是所有不包含特征 i 的特征的幂集。Delta(S, i)* 是将特征 i 添加到特征联盟 S(这是幂集 C 中的一个集合)时导致的预测变化。
该方程总结为“将特征 i 的加权边际贡献添加到不包括 i 的每个可能特征联盟中”。
KernelSHAP 处理的问题是,随着特征数量的增加,幂集大小呈指数级增长,这使得计算变得极其庞大。KernelSHAP 是通过解决以下问题来计算的:
方程 2:KernelSHAP 方程 [3]
其中 h_x 是应用于 z 的掩码函数,z 是从集合 Z 中采样的二进制向量,代表所有可能的特征联盟的集合。这个函数将由 z 表示的联盟映射到掩码数据点,然后将其输入到我们的模型中(f_theta)。目标是找到最佳的线性模型 (g),该模型在所有掩码下估计模型性能。线性模型中的权重是 KernelSHAP 值。这一切都可以通过以下定义的组合核实现:
方程 3:组合核 [3]
要计算 VARSHAP,只需将 g 的线性表示替换为 VAR 模型。根据作者的说法,由于线性模型和 VAR 模型的系数都是通过普通最小二乘法估计的,因此所有 KernelSHAP 的数学原理仍然适用,并且对时间序列更具代表性 [3]。
时间一致性 SHAP
如前所述,SHAP 是一种将模型的特征解释为游戏中的玩家,并使用 Shapley 值来寻找公平奖励分配的方法。然而,对于随时间发展的游戏,这些分配可能不足以激励所有方追求最初目标。为了避免这种情况,博弈论者使用分配计划和时间一致性的概念来管理跨时间的激励 [3]。
这种通过时间的利益竞争思想也扩展到特征上,因为传统的 SHAP 方法将不同时间步的相同特征视为游戏中的不同玩家。根据作者的说法,我们可以通过添加时间一致性来弥合这一差距 [3]。
SHAP 值的时间一致性可以表示如下:
方程 4:Shapley 值的时间一致性 [3]
在这个方程中,beta 代表在 t 时间步中对玩家(特征)i 进行的支付分配计划,而 phi(0,i) 是玩家对游戏(预测)贡献的总值。可以将其视为类似于商业伙伴关系。
每个个体(即特征)向启动基金(在时间步 0 时为 phi)支付初始金额。然后,在未来的时间步中,个体会定期获得回报,因为他们对业务结果(即最终预测)作出更多贡献。这些支付也会使任何个体不愿意采取不利于业务利益的行动。通过这种方式表述问题,TC-SHAP 在时间序列背景下表现得更好,因为现在 特征的不同时间步被建模为一个整体,而不是作为单独的参与者。
在实际操作中,可以采取以下步骤:
-
通过将特征替换为零或特征均值来计算每个特征的总 SHAP 贡献(在时间步 0 时为 phi)。对所有特征重复此操作。
-
然后我们需要计算窗口内每个时间步的“子游戏 SHAP”。这通过修改方程 2 中的掩码机制来完成,即不再只掩码时间步 (t-w),而是掩码 t-w 和 t 之间的所有时间步(再次使用零或均值)。
-
然后我们简单地使用方程 4 和 5 计算填充值表。
方程 5: 填充值表 [3]
第 1 步计算“初始投资”。第 2 步则实施了一个观点,即我们有 N 个特征跨越多个时间步 (W),而不是 NW* 个特征。第 3 步通过提供填充值表(或每个“投资者”的周期性回报)将所有内容汇总在一起。
这个过程还有一个好处,即将计算次数从 2*^(NW) 减少到 *W**2^N,其中 W 是用于预测的时间步数,而 N 是特征的数量 [3]。
一旦计算完成,我们可以将 TC SHAP 值解释为“在给定时间步中,特征的演变如何影响其他特征轨迹的联盟。” 换句话说,TC SHAP 代表了给定时间步的特征如何改变未来时间步中其他特征的共同贡献。对未来协作产生重大影响的特征-时间步点将根据定义对最终预测产生重大影响。
使用 TC SHAP 进行事件检测
虽然了解单次预测中某个时间步的重要性是有用的,但时间序列分析通常涉及分析多个预测和模式,我们可能想要了解模型预测中一些重要时间步的总体情况 [3]。
根据作者的说法,我们可以通过将给定预测集(或如果我们想要全球事件检测机制则是所有预测)的 TC SHAP 值相加来找到重要的时间步。通过绘制这些值,我们可以轻松查看哪些时间步很重要以及一些重要事件可能发生在哪里 [3]。
作者通过 Individual Household Electricity dataset 演示了这种方法的有效性。作者训练了一个 LSTM 网络,后接一个密集层来预测功耗。然后,他们计算了 TC-SHAP 值并将其汇总以获得事件检测卷积。接着,他们将卷积叠加到目标时间序列上。
图 1:事件检测卷积(蓝色)用于子计量 2 和 3 与目标的比较(图源自 [3])
如图 1 所示,目标变量的大幅变化可以通过事件卷积中的大幅峰值来解释。例如,在时间步 25 之后,子计量 2 的事件卷积出现了一个大峰值,随后目标也出现了大幅下降。类似地,在时间步 75 左右,子计量 3 的事件卷积大幅下降后,目标也出现了大幅下降。大多数的大幅变化可以通过一些子计量的变化来解释。
结论
对 KernelSHAP 的修改填补了当前工作中的一个巨大空白。除此之外,针对时间序列特征重要性进行开发的事后可解释性方法并不多。TC-SHAP 有助于解决这个问题,确实是迫切需要的。
然而,这种新方法仍然存在一些关注点和进一步的工作需要解决。其中一个关注点(作者也提到)是 VARSHAP 和 TC-SHAP 解释之间的显著差异,这表明需要更多的工作来检验这些值的确切解释。此外,尽管理论上 TC-SHAP 克服了独立性问题,但仍需更多实验来完全确认这一说法。
此外,一般来说,模型无关的方法可能具有误导性,因为它们只能提供重要性的估计,而非真实的重要性。然而,对于大多数使用情况,这种粗略的评估已经足够,并且拥有一个处理时间依赖性的方法非常有用。
资源和参考文献
-
Python 的 SHAP 包:
shap.readthedocs.io/en/latest/index.html
-
对 Kernel SHAP 的更深入解释:
christophm.github.io/interpretable-ml-book/shap.html#kernelshap
参考文献
[1] L. Shapley. n 人博弈的价值。(1953)。博弈理论贡献 2.28。
[2] S.M. Lundberg, S-I. Lee. 一种统一的模型预测解释方法。(2017)。神经信息处理系统进展,30。
[3] M. Villani, J. Lockhart, D. Magazzeni. 时间序列数据的特征重要性:改进 KernelSHAP (2022)。ICAIF 解释性人工智能在金融中的研讨会
SHAP 与 ALE 在特征交互上的对比:理解冲突的结果
模型解释工具需要深思熟虑的解读
·发布于Towards Data Science ·10 分钟阅读·2023 年 10 月 2 日
–
图片由Diogo Nunes拍摄,发布在Unsplash
在这篇文章中,我比较了特征交互的模型解释技术。令人惊讶的是,两个常用的工具,SHAP 和 ALE,产生了相反的结果。
我可能不应该感到惊讶。毕竟,解释工具以不同的方式测量特定的响应。解释需要理解测试方法、数据特征和问题背景。仅仅因为某物被称为解释器并不意味着它生成了解释,如果你把解释定义为一个人理解模型的工作方式。
本文重点关注特征交互的解释技术。我使用了一个来源于真实贷款的常见项目数据集[1],以及一种典型的模型类型(一个提升树模型)。即使在这种日常情况下,解释也需要深思熟虑的解读。
如果忽略了方法论细节,解释工具可能会妨碍理解,甚至破坏确保模型公平性的努力。
在下文中,我展示了不同的 SHAP 和 ALE 曲线,并证明这些技术之间的分歧来源于测量响应和测试执行的特征扰动的差异。但首先,我会介绍一些概念。
特征交互
特征交互发生在两个变量共同作用时,导致的效果与它们各自贡献的总和不同。 例如,一夜的睡眠质量差对第二天的测试成绩的影响会大于一周后的影响。在这种情况下,代表时间的特征将与睡眠质量特征相互作用或修改。
在线性模型中,交互表示为两个特征的乘积。非线性机器学习模型通常包含众多交互。事实上,交互是高级机器学习模型逻辑的基础, 然而许多常见的可解释性技术侧重于孤立特征的贡献。检查交互的方法包括 2-way ALE 图、Friedman 的 H、部分依赖图和 SHAP 交互值 [2]。本博客探讨了其中的两个:ALE 和 SHAP。
ALE 图
累计局部效应(ALE)是一种测量特征效应的技术,不会受到相关特征或不太可能的特征组合造成的失真的影响。 特征交互可以通过 2-way ALE 图进行可视化。2-way ALE 图首先通过扰动一个特征(i),在固定的第二个特征(j)值下测量模型输出的变化。然后,(在稍微不同的 j 值下进行类似的测量。这两个测量值的差异揭示了扰动 j 如何影响模型对 i 变化的响应。为了减少不太可能的特征组合的影响,测量仅使用在选择的 i 和 j 值附近的小窗口中的观察值。
SHAP 交互值
Shapley 值表示每个特征对模型输出的信用或责任的量。 “SHAP”指的是一组用于计算机器学习模型的 Shapley 值的方法。SHAP 计算测量特征设置为其原始值与参考值相比时模型响应的变化。特征的边际贡献是通过对其他特征的各种组合或“联盟”进行平均来计算的。Shapley 联盟是通过将一些特征值替换为从参考数据集中(通常是训练数据)随机抽取的值来形成的。与 ALE 不同,Shapley 涉及许多特征的扰动,而不仅仅是感兴趣的特征对,并且为每个观察值计算值。
SHAP 交互值将模型分数分配到所有特征主效应 和 成对交互 [3]。对于一个观察值,特征对(i 和 j)的交互是通过测量在特征 i 具有其原始值 j 时的 Shapley 值来计算的。然后,将 j 替换为从参考中随机抽取的值,并计算特征 i 的新 Shapley 值。这两个测量值之间的差异量化了特征 j 如何修改 i 的 Shapley 值。
数据和模型
我使用了通过 Kaggle [1] 获得的 Lending Club 贷款数据集。该模型根据利率、贷款期限、借款人收入、借款人住房拥有状态以及个人或联合借款人的信用评分等特征预测贷款违约。利用其他人 [1, 4–5] 做的分析,我选择了 18 个预测特征,响应变量是贷款违约的二元指标。训练了一个提升树模型,使用 Scikit-learn 的 GradientBoostingClassifier,该模型与 ALE 图 (PyALE)、SHAP 值 (SHAP) 和 Friedman 的 H (sklearn_gbmi) 兼容。代码可在 GitHub [6] 上获得。
SHAP 和 ALE 对有影响的特征意见不一致
为了检查模型中的交互作用,我生成了 SHAP 依赖图和二维 ALE 图用于特征对。对于大多数特征对,ALE 和 SHAP 图至少有一些相似之处。但对于一个关键交互作用,即利率和期限,结果却出现了冲突:
图 1. 利率和期限的交互作用度量。正值表示预测的违约风险更高。x 轴显示利率,颜色对应于期限值;红色表示 60 个月,蓝色表示 36 个月。A. 利率:期限交互作用的 ALE 值折线图。B. 显示利率:期限的 SHAP 交互作用值的散点图。图片来源于作者
二维 ALE 表明,与高利率结合的较长期限增加了风险。但 SHAP 讲述了相反的故事;较长期限在高利率下对违约有保护作用!
在这个数据集中,贷款期限(term)是分类变量,只有两个值,36 个月和 60 个月,而利率(int_rate)是连续变量。图 1A 和 B 分别显示了 ALE 和 SHAP 值,这些值绘制在相同的尺度上,正值表示由于交互作用,模型默认风险增加。尽管热图通常用于二维 ALE 图,但我更喜欢折线图;这些图也更容易与 SHAP 图进行比较。
图 1 中的数据矛盾尤其令我担忧,因为利率和期限是模型中两个最重要的特征,通过多种衡量标准(聚合 Shapley 值、不纯度和置换重要性;见[6])。此外,根据 SHAP、ALE 和 Friedman 的 H,期限:利率交互作用也很大。
因此,我有两个具有重要交互作用的有影响力特征,但 SHAP 和 ALE 显示的效果方向不同。常识能帮助解决冲突吗?以下是曲线的一些可能解释:
(ALE) 长期高利率贷款特别具有风险。
(SHAP) 高利率对违约的预测力很强;在高利率时,期限并不那么重要。
(SHAP 的故事也来源于单向 ALE 响应[6]。负交互作用取消了单向项的响应,因此这种交互作用可以被解释为该项特征影响力的丧失。)
作为一个非借贷领域的专家,这两个账户对我来说似乎都很合理。理解这些图形为何有所不同意味着深入理解这些技术,同时也要考察简化模型。
Mike Houser的照片,来源于Unsplash
SHAP 与 ALE — 哪些差异是重要的?
SHAP 交互作用和双向 ALE 值都测量当特征j被修改时,模型响应的差异,针对特征i具有相似值的数据点子集。
从上述陈述开始,让我们列出 SHAP 和 ALE 可能存在差异的一些方式:
1. 选择用于测量的数据点。
2. 测试所测量的响应。
3. 特征值如何被修改。
第 1 项似乎不太可能是罪魁祸首。对于 Shapley 来说,是对每个数据点进行测量,我们使用原始特征值。ALE 考虑的是一个值周围的窗口。窗口大小是基于数据密度的,因此较高的利率点反映了一个相对较大的值范围,但在图 1 的“高利率”部分,我们可能有足够相似的观测值。
第 2 项和第 3 项的差异可能对解释图形中的不一致很重要。对于第 2 项,SHAP 和 ALE 测试不同的模型响应。ALE 使用原始模型输出,而 SHAP 则将模型预测分布到多个特征上,并检查归因于 i的部分。
对于第 3 项,ALE 扰动仅涉及特征j的值替代。而 SHAP 则汇总了许多模型特征的响应;所有变量都会被扰动。替代值是从训练数据中随机抽取的,通常反映了更典型的值,这可能与初始观测的特征差异很大。
稀有情况生成 ALE 信号
在进行模型简化和其他分析后(见[6]中的代码),我意识到 ALE 测试是在响应多个风险因素的情况下模型的预测。下面,我重新计算了仅针对年收入超过 45,000 美元且不是租房者的客户的 ALE 和 SHAP 图。
图 2. 利率和期限的交互测量,仅对高收入非租户客户计算。P. 正值表示预测的违约风险较高。x 轴显示利率,颜色对应期限值;红色表示 60 个月,蓝色表示 36 个月。A. 利率:期限交互的 ALE 值折线图。B. 显示利率:期限的 SHAP 交互值的散点图。图片由作者提供。
当排除低收入和租户案例(约占总数的 50%)时,ALE 信号几乎完全消失,而 SHAP 曲线在质量上保持不变。
原始的 ALE 曲线(图 1A)可以通过一个简化的单树模型进行再现,该模型只涉及三个特征(利率、期限和年收入),如下所示:
图 3. 简单模型以重现 ALE 检测到的特征交互。A. 单个决策树模型的图示。树的遍历从节点 0 开始,当框中显示的条件为真时向左移动,否则向右移动。遍历在叶节点结束;响应是该节点中的值。根据模型响应值对框进行着色(对于非叶节点,值反映了子叶节点的平均值)。在框中注明了达到每个节点的训练数据样本数量。B 关于利率:期限的 ALE 值的折线图,展示了 A 中所示的树。C. 显示利率:期限的 SHAP 交互值的散点图,针对 A 中的树。由于文本中讨论的原因,一些异常点在 SHAP 曲线中被裁剪。图片由作者提供。
图 3A 包含具有非常高或低值的低人口节点(例如,节点 2 和 7)。节点 7 由稀有客户访问,这些客户具有低收入、高利率和长期;这些客户的违约风险非常高。
ALE 图受到稀有特征组合影响的主导作用。 节点 7 代表了少量的贷款,但在 ALE 计算过程中,当更改期限时,模型响应发生了剧烈变化。60 个月的客户离开此节点,降低了风险,而(数量更多的)36 个月客户进入此节点,导致了一个显著的信号。
SHAP 检测复杂模型中的系统性效应
图 1B 中的 SHAP 信号在图 3B 中消失。模型复杂性是 SHAP 结果的关键。为了可靠地再现原始的 SHAP 曲线,我发现需要≥4 个特征、20 棵树和深度>5(见[6]中的代码)。
图 3B 中的 SHAP 曲线包含了超过 30% 利率的异常值(其中一些在图中被裁剪;异常值高达 ~0.4)。如果对异常值取平均,36 个月和 60 个月的值非常相似,接近零 (~0.001)。这些异常值是由于具有访问极端节点 4 和 7 的联合体的情况。模型复杂性减少了异常值。随着特征数量的增加,从参考数据中抽取多个不寻常的值变得不太可能。此外,在计算中平均更多的联合体会稀释信号。
SHAP 测量会降低对稀有特征组合的重视。 SHAP 联合体可能涉及与原始特征值非常不同的值,而 ALE 计算通常涉及在更受限范围内的扰动。SHAP 联合体提供了对模型的更广泛覆盖,反映了由更多节点生成的值,特别是人口较多的节点。
SHAP 计算中特征变化的范围取决于观察值是否相对于参考数据异常。图 1B 和 2B 中的 36 个月期限曲线的平坦性反映了大多数客户(75%)拥有 36 个月的贷款。因此,为期限生成 SHAP 联合体时,随机抽取的值可能会使期限保持不变。减去两个相似曲线的结果会得到一个较小的 SHAP 交互值。
相比之下,60 个月的期限曲线与典型情况更远,因此生成 SHAP 信号。高利率和 60 个月期限下的负值表明,利率特征对 36 个月较低期限值的影响更大。更多的贷款是 36 个月的,并且大多数贷款风险适中,因此在这种情况下高利率更令人惊讶。对于 60 个月期限,高利率则不那么令人惊讶(利率和期限的 Pearson 相关系数约为 0.4),因此可能预期 SHAP 对长期贷款的利率特征赋予较少的权重。
那么,哪个是正确的?
之前,我描述了图 1 中曲线所暗示的两个不同故事:
(ALE) 长期高利率贷款特别具有风险。
(SHAP) 高利率对违约的预测能力很强;当利率较高时,期限并不是很重要。
这两个故事似乎都是真的,但针对不同的客户。对于具有多个风险因素的少见情况,第一个解释是正确的;利率和期限的组合会产生非常大的 ALE 响应。但对于更典型的高利率客户,利率捕捉了大部分风险。因此,SHAP 和 ALE 测试关注的是不同的客户。
为什么这很重要?
在应用可解释性工具后,我们期望能增加对模型工作原理的理解。我们相信,我们将对模型的决策过程有一个总体的了解,甚至可能揭示数据中的一些模式。这些测试用于质量控制和信任建立。当解释与期望一致时,利益相关者会感到安心。
可解释性工具可以提供许多好处,但它们也可能误导或提供虚假的安慰。
可解释性工具在模型公平性测试中尤其重要,以避免偏见和歧视。当特征偏见存在或怀疑存在时,交互度量是至关重要的[7]。SHAP 对稀有特征组合的缺乏响应可能是一个问题,因为性别、种族或年龄等特征组合可能与不良结果有关。相反,ALE 可能会忽略系统性效应,因为它扰动了较少数量的特征,范围更有限。
结论
模型可解释性包通常被描述为“解释器”,其输出为“解释”。我认为使用测试或测量这样的词更为有用。例如,“SHAP 值”比“SHAP 解释”更好,因为包的输出与对复杂模型的实际理解之间存在一些距离。我正在尝试改变我对这些术语的使用,以提醒自己这一点!
在医学中,诊断测试在特定情况下进行,结果由专家解读。通常,为了建立诊断,使用不止一种测试。同样,需要对模型可解释性工具有更深入的理解才能得出有意义的结论。
参考文献
[1] N. George, All Lending Club loan data, www.kaggle.com/datasets/wordsforthewise/lending-club
。
[2] C. Molnar, 可解释的机器学习:使黑箱模型可解释的指南(2023)。
[3] S. M. Lundberg, G. G. Erion 和 S-I. Lee, 树集成的个体化特征归因(2019),arXiv:1802.03888v3 [cs.LG]。
[4] M. Gusarova, 特征选择技术(2023),Kaggle。
[5] N. George, 使用 Python 进行探索性数据分析(2019),Kaggle。
[6] V Carey, GitHub 代码库, github.com/vla6/Blog_interactions
。
[7] V Carey, 特征偏见的无免费午餐(2021),Towards Data Science。
使用 ONets 进行形状重建
原文:
towardsdatascience.com/shape-reconstruction-with-onets-1c1afe89c50
使用可学习函数表示 3D 空间
·发布于 Towards Data Science ·11 分钟阅读·2023 年 2 月 7 日
–
(照片由 Tareq Ajalyakin 拍摄,来源于 Unsplash)
3D 重建的问题旨在根据物体的噪声视图(例如,部分点云、2D 图像等)生成高分辨率的物体表示。最近,深度神经网络成为 3D 重建的热门方法,因为它们可以编码有助于处理模糊的信息。简单来说,这意味着如果从输入中不清楚如何正确重建给定的物体,神经网络可以借鉴它在训练过程中遇到的其他数据点的经验,仍然生成合理的输出。
大多数 3D 重建方法最初在表示高分辨率物体的能力上存在限制。体素、点云和网格在以内存高效的方式建模高分辨率物体方面都存在不足。与如 GANs [2] 等可以生成高分辨率、逼真图像的模型相比,用于生成 3D 几何的可比方法还处于初级阶段。
(来自 [1])
为了解决这个问题,文献[1]中作者提出了一种 3D 重建方法,该方法使用神经网络对占据函数进行建模。更具体地说,我们训练一个神经网络来预测空间中给定点是否被物体占据(即,占据函数!)。然后,底层物体通过该神经网络的决策边界(即,预测从占据到未占据的切换位置)来表示;见上文。占据网络(ONets)可以以任意精度和合理的内存要求表示和重建 3D 形状。
(来自 [1])
背景
在我们之前对DeepSDF的 3D 形状生成的回顾中,我们涵盖了一些相关的背景概念,包括:
[1]中的大多数神经网络使用了简单的前馈架构,这种架构作为一种替代方案,用于存储 3D 形状的标准网格、体素或点云格式。为了理解为什么这种方法更可取,我们需要更详细地了解这些表示的限制。
3D 形状表示的缺点
(来自 [6])
3D 几何形状通常以网格、体素或点云的形式存储或表示;见上文。鉴于这些表示已经存在,我们为什么还要使用神经网络来表示形状呢? 简单的答案是 (i) 其他表示方法存在一些显著的限制,以及 (ii) 我们可以通过这种方式节省大量内存。
体素在内存使用上不高效。 在深度学习应用中,体素是用于 3D 形状的最广泛使用的表示形式,因为它们简单——它们是像素在 3D 空间中的直接推广。然而,如果我们使用体素来编码一个 3D 形状,这种编码的内存占用会随着分辨率的提高而立方增长。如果我们想要更精确的体素表示,我们需要使用更多的内存。
点云是断开的。 点云的格式类似于我们通常从传感器(如 LiDAR)获得的数据,但结果几何形状是断开的——它只是 3D 空间中的一堆点。因此,从这些数据中提取实际的 3D 形状需要昂贵的后处理程序。
网格并不是解决方案。 如果点云需要后处理而体素在内存使用上不高效,我们应该使用网格,对吗? 不幸的是,大多数网格表示实际上是基于变形的“模板”网格。实际上,这意味着网格无法编码任意的拓扑结构,这使得它们在准确表示某些几何形状时受到限制。
那么我们该怎么办? 考虑到这些限制,[1]中提出的方法开始变得更加合理。我们可以训练一个神经网络来生成可以恢复形状的输出,而不是直接存储 3D 形状的网格、体素或点云表示。通过这种方式,我们可以在一个具有固定内存成本的神经网络参数中存储大量不同的几何形状!
占用函数
[1]中的工作将占据函数表示为神经网络,但占据函数是什么?简单来说,这只是一个将空间中的点(例如,[x, y, z]
坐标)作为输入,并返回一个二进制输出的函数,表示该位置是否被目标对象“占据”。
占据函数的特征
这样的函数可以通过一个神经网络进行逼近,该网络被训练为在给定 [x, y, z]
坐标作为输入时输出零到一之间的概率。
提取等值面。 要从占据函数中提取 3D 几何体,我们必须找到一个等值面。为此,我们只需在 3D 空间中找到占据函数等于某个阈值 t
的点。
等值面
在这里,t 被设置为零到一之间的某个值。因此,等值面表示占据函数的边界,即输出从零切换到一(或反之)的地方——这对应于基础对象的表面!
评估指标
用于判断 3D 形状质量的指标与我们在普通计算机视觉中看到的指标非常相似。[1]中使用的主要指标如下所述。
体积 IoU。 两个形状交集的体积除以它们并集的体积。该指标与法线 IoU相同,但它已被推广到三维。
Chamfer-L1。 精度和完整性的均值。精度是输出网格上点到地面真实网格最近点的平均距离。完整性是相同的,但方向相反。
法线一致性。 我们取预测网格的面法线(即一个垂直于网格某一面平面的向量),找到另一个网格中对应最近邻的面法线,然后取这些向量的点积的绝对值。通过对预测网格中的所有法线重复这一过程并取平均值,我们得到法线一致性。这一指标稍显复杂,但它对于确定预测形状是否捕捉到高阶信息(即两个网格的面是否趋向于指向相同方向)非常有用。
占据网络 [1]
在了解 DeepSDF 的基本概念后,我们可能会问占据网络的第一个问题是:为什么要建模占据函数,而不是像 有符号距离函数(SDFs)这样的替代方法? 基本的答案是,占据函数更容易学习。
“[SDFs 通常比占据表示更难学习,因为网络必须在 3D 空间中推断距离函数,而不仅仅是将体素分类为占据或未占据。]” — 摘自 [1]
网络。 为了逼近占据函数,我们使用一个前馈神经网络,该网络将一个介于零和一之间的概率分配给任何 3D 坐标。网络的输入包括:
-
一个(单一的)3D 坐标。
-
对底层物体的噪声观察。
我们称之为 ONet 的网络输出一个标量概率。作为模型输入的底层物体的噪声观察可能是诸如不完整的点云或粗略的体素网格之类的东西。我们将神经网络以这些噪声数据为条件,然后使用它生成物体的更精确表示;见下文。
对每个空间位置建模占据函数
我们不能直接将观察到的噪声数据作为输入传递给前馈神经网络,因为它们可能有许多不同的格式。相反,[1]中的作者使用不同的基于神经网络的编码器(例如,图像的 ResNets [4] 或点云的 PointNet [5];这些只是将图像和点云转换为向量的常见网络架构)将这些数据(即,将其转换为向量)嵌入。
然后,前馈神经网络的第一层使用 条件批量归一化 — 一种条件 批量归一化 变体 — 根据物体嵌入调整网络的输入。这样,我们确保 ONet 的输入以我们尝试重建的 3D 几何数据为条件。
训练。 为了训练 ONet,我们 (i) 考虑一个围绕训练物体的填充空间体积,以及 (ii) 在这个空间中均匀采样 K
个占据函数值。通过将这种采样过程应用于多个训练物体来形成一个小批量。我们使用带有每个小批量中占据函数值的 交叉熵损失 的小批量梯度下降法正常训练网络。
生成对象。 一旦 ONet 训练完成,我们可以在任意分辨率下输出给定空间位置的占据函数值。但是,我们如何使用这些值来创建实际的 3D 对象(例如,以网格格式)? 为此,[1]中的作者提出了一种多分辨率等值面提取(MISE)过程。该过程受到 八叉树 的启发,八叉树是一种递归表示 3D 空间的树形数据结构。每个八叉树节点有八个子节点,我们可以递归地向每个节点添加子节点,以表示更高分辨率的体积。
MISE 的基本过程如下:
-
在初始分辨率下离散化空间
-
在该空间的每个离散位置评估 ONet
-
标记所有具有至少两个相邻体素且占用情况不同的体素
-
将标记的体素划分为八个子体素,并重复直到达到所需的分辨率
-
应用 Marching Cubes 以获取网格
因此,MISE 通过在需要的更高分辨率下递归评估 ONet 来获得网格(即,接近对象边界)。尽管我们必须应用一些额外的步骤来精细化此网格,但整体过程相当直观;见下文。
(来自 [1])
实验结果。 ONets 主要在合成 ShapeNet 数据集上进行评估,基于其表示复杂 3D 形状和/或从图像、噪声点云和低分辨率体素网格中重建它们的能力。在这些实验中,我们看到 ONets 可以准确地表示 ShapeNet 的“椅子”部分。使用 ONet,我们可以使用仅 6M 参数独立编码近 5K 对象。相比之下,体素表示不够准确,并且其内存要求随着所需分辨率的增加而增加;见下文。
(来自 [1])
在重建实验中,我们继续看到 ONets 工作得相当好。它们能够恢复复杂的形状,并且往往产生最接近真实几何的结果。大多数基准方法通常存在限制,例如产生粗糙的表示、需要后处理的断开对象或缺乏相关细节的对象。模型输出的定性示例如下所示。
(来自 [1])
当尝试从噪声点云和粗糙体素网格(即,而不是像上面那样使用图像)重建几何时,我们看到 ONet 继续表现良好;见下文。
(来自 [1])
其他内容。 ONets 还通过从 KITTI 和 Online Products 数据集中获取图像应用于现实世界数据。尽管仅在合成数据上训练,ONet 似乎对这种类型的数据泛化效果很好。然而,值得注意的是,作者确实使用了 KITTI 提供的分割掩码来提取与所需对象相关的像素。以下展示了从这些数据集中生成的重建示例。
(来自 [1])
超越[1]中的初步提议,作者还提出了一种生成版本的 ONet,该版本通过对 ShapeNet 进行无监督训练,并形成一个类似于变分自编码器(VAE)的 3D 几何潜在空间。简单来说,作者发现可以创建生成 ONets,这些 ONets 能够生成新的网格并在网格之间进行插值。如果我们希望关注生成应用而不仅仅是 3D 重建,这将非常有用。
要点
在 ONets 之前,现有的 3D 重建方法在保持合理内存占用的同时,难以对高分辨率物体进行建模。在[1]中,我们了解到,更智能的 3D 几何表示可以带来显著的好处。ONets 提案中的主要要点如下。
占据函数非常棒。 常见的 3D 几何表示(如网格、点云、体素)在表示高分辨率物体时往往占用过多内存。占据函数是一种更简洁的表示方法,它通过编码单一函数来实现对 3D 物体的任意精度建模。而且,与像 SDF 这样的替代方法相比,占据函数更容易学习或建模。
可学习的重建。 当然,占据函数很棒,但这项工作的真正价值在于我们如何表示这些函数。也就是说,我们可以使用神经网络来学习和存储各种形状的占据函数。因此,我们可以通过*(i)训练一个 ONet 和(ii)*存储模型的参数,以任意精度表示许多不同的形状。这种方法使用有限且固定的内存量,并通过利用先验信息来提高重建质量!
表示 3D 空间。 在解释使用 ONets 生成网格的 MISE 方法时,我们很快遇到了八叉树的概念。这是 3D 建模中的一个重要数据结构,它允许我们以不同的精度递归生成形状。如果我们想获得更精确的表示,只需继续细分体素。但我们应仅在有意义的地方(即当附近的体素具有不同的占据情况时)进行此操作,以避免不必要的计算。
结束语
非常感谢阅读本文。我是Cameron R. Wolfe,一名在Alegion工作的研究科学家,同时也是莱斯大学的博士生,研究深度学习的经验和理论基础。你也可以查看我在 medium 上的其他写作!如果你喜欢这篇文章,请在twitter上关注我或订阅我的Deep (Learning) Focus 通讯,我在其中撰写了有关深度学习重要主题的易懂概述系列。
参考文献
[1] Mescheder, Lars, 等. “占据网络:在函数空间中学习 3D 重建。” IEEE/CVF 计算机视觉与模式识别会议论文集。2019 年。
[2] Goodfellow, Ian, 等. “生成对抗网络。” ACM 通讯 63.11 (2020):139–144。
[3] Mildenhall, Ben, 等. “NeRF:将场景表示为神经辐射场以进行视图合成。” ACM 通讯 65.1 (2021):99–106。
[4] He, Kaiming, 等. “图像识别的深度残差学习。” IEEE 计算机视觉与模式识别会议论文集。2016 年。
[5] Qi, Charles R., 等. “Pointnet:针对 3D 分类和分割的点集深度学习。” IEEE 计算机视觉与模式识别会议论文集。2017 年。
[6] Hoang, Long, 等. “一种使用波形核签名和 3D 三角网中心点的 3D 对象分类深度学习方法。” 电子学 8.10 (2019):1196。
用 SQL 进行数据塑形
原文:
towardsdatascience.com/shaping-your-data-with-sql-71822f2fc2f4
使用不同的技术改进和优化你的数据分析过程
·发表于Towards Data Science ·9 分钟阅读·2023 年 4 月 18 日
–
什么是数据塑形?
没有一种放之四海而皆准的数据。为了不同的目的和使用案例,数据会根据需要进行定制。你对数据未来使用目的的了解越多,你就越能正确地将数据呈现给最终用户。
例如,用于进行深入分析的数据与提供给高层管理的汇总数据有所不同。
另一个例子是,业务发展团队更关心每个地区的总体成本以吸引新用户,而市场营销经理则更关注与区域有关的附属营销成本。
也就是说,将现有数据结构转换为任何替代的透视或非透视结构,是数据操作和分析过程中不可或缺的一步。
在这篇文章中,我将介绍一些在特定情况下对数据进行塑形和切片的技术。通常,我会使用 PostgreSQL 来展示我的例子。
现在,让我们开始看看我们得到的结果吧!
数据
在这篇文章中,我将使用《2015–2021 年世界幸福报告》的数据。该数据集提供了基于不同指标的全球各国幸福水平:经济增长、社会支持、出生时的预期寿命等。数据可在Kaggle上获得,并具有CC0: 公共领域许可。如下面的图像所示,我将仅利用一些字段:
-
国家名称
-
年份:报告年份(2005–2021)
-
生活梯度:每个受访者认为的最佳生活由梯度上的 10 表示,而最差的生活由 0 表示。然后要求每个参与者在梯度上对其当前生活进行排名。
-
人均 GDP 对数:以购买力平价(PPP)调整的美元表示的人均 GDP 的对数。
-
社会支持:国家中对社会支持(能够依靠他人)的感知。
-
健康预期寿命:指一个国家公民在特定时期的平均寿命。
作者提供的图片
使用窗口函数
使用 PRECEDING AND CURRENT ROW 进行的滚动计算
示例: 显示每个国家的滚动三年平均生活梯度指数
什么是三年滚动平均?简单来说,它计算的是过去两年的平均生活梯度分数加上当前年。如果当前年份是 2010 年,例如,每个国家的三年滚动平均生活梯度分数将等于该国家在 2008 年、2009 年和 2010 年的分数的平均值。如下面的图片所示,2010 年阿富汗的‘rolling_average’为 4.29,这个值是 3.72、4.40 和 4.76 的平均值。
作者提供的图片
为了指定计算中要考虑的行范围,SQL 中的窗口函数与PRECEDING
和CURRENT ROW
一起使用。具体来说,PRECEDING
确定在CURRENT ROW
之前的行数。因此,与PARTITION BY
国家和ORDER BY
年份结合使用时,下面的 SQL 命令将返回每个国家在‘rolling_average’列中的滚动三年平均生活梯度分数。
SELECT year, country_name, life_ladder,
AVG(life_ladder) OVER (
PARTITION BY country_name
ORDER BY year
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
) AS rolling_average
FROM public.happiness_index;
使用 UNBOUNDED PRECEDING AND CURRENT ROW 进行的周期计算
示例: 显示每个国家的期间平均生活梯度分数
为了进行此计算,我们使用UNBOUNDED PRECEDING AND CURRENT ROW
。计算窗口将包括当前值和所有行直到当前行。
例如,如果当前年份是 2009 年,那么一个国家在这段时间内的平均分数将等于其 2008 年和 2009 年的分数的平均值。类似地,在 2010 年,平均生活梯度分数将通过将 2008 年、2009 年和 2010 年的分数除以 3 来确定。你可以参考下面的命令获取更多信息。
SELECT year, country_name, life_ladder,
AVG(life_ladder) OVER (
PARTITION BY country_name
ORDER BY year
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS rolling_average
FROM public.happiness_index
作者提供的图片
使用 UNBOUNDED PRECEDING AND CURRENT ROW 进行的百分比计算
示例: 计算生活梯度点与累积平均值的百分比变化。
类似于之前的示例,使用了UNBOUNDED PRECEDING AND CURRENT ROW
,但这个示例并不是仅计算跨时间的平均值,而是关注当前值与期平均值之间的百分比变化。在这种情况下,结果存储在第四列,如下图所示。你可以很容易地观察到哪一年相比滚动平均值实际发生了正/负变化。此外,这个指标还告诉我们目标值变化的幅度。
图片由作者提供
SELECT
year,
country_name,
life_ladder,
100 * (life_ladder - AVG(life_ladder) OVER (PARTITION BY country_name ORDER BY year ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)) / AVG(life_ladder) OVER (PARTITION BY country_name ORDER BY year ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS percentage_change
FROM
public.happiness_index
实际上,在时间序列分析中,这种方法是识别数据趋势或模式的重要方法之一。例如,它在销售分析中使用最为频繁,主要关注销售增长或市场份额。
使用 PERCENTILE_RANK() 进行百分位计算
示例: 识别区域内每个国家的人均 GDP 的百分位
在这种情况下,我想知道哪些国家在 2008 年各自区域中拥有更高的人均 GDP。这个任务只需借助PERCENTILE_RANK()
函数即可完成。
SELECT country_name, regional_indicator, log_gdppercapita,
round((PERCENT_RANK() OVER (
PARTITION BY regional_indicator
ORDER BY log_gdppercapita
))::numeric,2) AS percentile_rank
FROM public.happiness_index
where year = 2008;
图片由作者提供
因此,正如你所观察到的,通过PARTITION BY
区域和ORDER BY
人均 GDP,命令将国家根据其人均 GDP 排名分为不同的百分位类别。例如,根据数据,拉脱维亚的人均 GDP 高于中东欧地区 33% 的国家。
使用 CASE WHEN 结合聚合函数进行数据转换
示例: 显示每年每大洲的平均生活阶梯得分
在这种情况下,我将根据‘regional_indicator’字段将国家划分为亚洲、欧洲、非洲和美洲四个大区域。我们可以通过首先使用CASE WHEN
识别每个区域,然后得到每个位置对应的生活阶梯平均值。
SELECT year,
ROUND(AVG(CASE WHEN regional_indicator LIKE '%Asia%' THEN life_ladder
ELSE null END)::numeric,2) AS Asia,
ROUND(AVG(CASE WHEN regional_indicator LIKE '%Europe%' THEN life_ladder
ELSE null END)::numeric,2) AS Europe,
round(AVG(CASE WHEN regional_indicator LIKE '%Africa%' THEN life_ladder
ELSE null END)::numeric,2) AS Africa,
round(AVG(CASE WHEN regional_indicator LIKE '%America%' THEN life_ladder
ELSE null END)::numeric,2) AS America
FROM public.happiness_index
GROUP BY 1
ORDER BY 1
图片由作者提供
你可以看到保存于行中的 4 个不同区域的生活阶梯得分已经被转换成 4 列。这种数据转换使得分析师可以轻松监控不同地点在特定年份的值变化。它还对跟踪某地区在一定时期内的数据变化非常有用。
使用 UNION ALL 进行数据反透视
示例: 反透视之前数据集中的表
我们如何将上一个示例中的表转换成下表?
图片由作者提供
分析需要灵活的数据转换技术,因为它使你能够从任何维度查看数据并获得更有洞察力的信息。正如你所看到的,前面的示例展示了结果数据表在垂直查看和水平查看数据时如何提供洞察。
在这种情况下,我将展示如何使用 UNION ALL
将数据反透视到其原始状态的简单转换。当使用 UNION ALL
时,需要注意的是,所有用于联合的组件的列数和数据类型必须兼容。
WITH tbl AS
(SELECT year,
ROUND(AVG(CASE WHEN regional_indicator LIKE '%Asia%' THEN life_ladder
ELSE null END)::numeric,2) AS Asia,
ROUND(AVG(CASE WHEN regional_indicator LIKE '%Europe%' THEN life_ladder
ELSE null END)::numeric,2) AS Europe,
round(AVG(CASE WHEN regional_indicator LIKE '%Africa%' THEN life_ladder
ELSE null END)::numeric,2) AS Africa,
round(AVG(CASE WHEN regional_indicator LIKE '%America%' THEN life_ladder
ELSE null END)::numeric,2) AS America
FROM public.happiness_index
GROUP BY 1
ORDER BY 1)
SELECT year,
'Asia' AS region
, asia AS avg_life_ladder
FROM tbl
UNION ALL
SELECT year,
'Europe' AS region
, europe AS avg_life_ladder
FROM tbl
UNION ALL
SELECT year,
'Africa' AS region
,africa AS avg_life_ladder
FROM tbl
UNION ALL
SELECT year,
'America' AS region
, america AS avg_life_ladder
FROM tbl
;
使用 UNPIVOTING 和 PIVOTING 函数进行数据透视
UNNEST 函数用于数据反透视
示例: 与前一个案例的要求相同
了解多种处理数据以产生相同结果的方法是非常重要的,因为这可以对数据进行更大的主动控制。也就是说,除了 UNION ALL
外,UNNEST()
也是另一种用于数据反透视的函数。使用 UNNEST()
可以将数组列转换为不同的行。
WITH tbl AS
(SELECT year,
ROUND(AVG(CASE WHEN regional_indicator LIKE '%Asia%' THEN life_ladder
ELSE null END)::numeric,2) AS Asia,
ROUND(AVG(CASE WHEN regional_indicator LIKE '%Europe%' THEN life_ladder
ELSE null END)::numeric,2) AS Europe,
round(AVG(CASE WHEN regional_indicator LIKE '%Africa%' THEN life_ladder
ELSE null END)::numeric,2) AS Africa,
round(AVG(CASE WHEN regional_indicator LIKE '%America%' THEN life_ladder
ELSE null END)::numeric,2) AS America
FROM public.happiness_index
GROUP BY 1
ORDER BY 1)
SELECT
year,
UNNEST(ARRAY['Asia', 'Europe', 'Africa', 'Ameria']) AS region,
UNNEST(ARRAY[asia, europe, africa, america]) AS life_ladder
FROM tbl
;
图片来源:作者
在我们的示例中,数组列 [‘Asia’, ‘Europe’, ‘Africa’, ‘Ameria’] 在反透视后被转换回行值。
CROSSTAB 函数用于数据透视
示例:与 CASE WHEN 示例相同的要求:显示每年每个大洲的平均生活等级得分
COSSTAB()
是一种智能的数据透视、转换和汇总方法,以矩阵格式显示数据。在这种情况下,我将使用这个函数将亚洲、欧洲、非洲、美洲和独立国家联合体的行值转换为不同的列。
SELECT * FROM crosstab(
'SELECT year, region, round(AVG(life_ladder)::NUMERIC,2)::FLOAT as life_ladder
FROM (SELECT *,
CASE WHEN regional_indicator LIKE ''%Asia%'' THEN ''Asia''
WHEN regional_indicator LIKE ''%Europe%'' THEN ''Europe''
WHEN regional_indicator LIKE ''%Africa%'' THEN ''Africa''
WHEN regional_indicator LIKE ''%America%'' THEN ''America''
ELSE ''Commonwealth_of_Independent_States'' END AS region
FROM public.happiness_index) a
GROUP BY 1,2
ORDER BY 1, 2') AS region_life_ladder (year int, Asia FLOAT, Europe FLOAT, Africa FLOAT, America FLOAT, Commonwealth_of_Independent_States FLOAT)
图片来源:作者
结论
数据下隐藏着洞察力,我们的使命是以任何可能的方式处理数据,以从数字和事实中获取最大价值。
以上是我在数据整理和处理中的一些技巧,希望它们对你有所帮助。
感谢你读到最后。要获取有关我即将发布的文章的更新,请通过提供的 Medium 链接 订阅成为会员。
你可以在以下网址阅读我的其他 SQL 文章:
-
nphchi223.medium.com/all-about-data-profiling-in-sql-582a0f250d75
-
medium.com/geekculture/essential-sql-queries-that-data-analysts-should-have-known-bec83a300193
-
towardsdatascience.com/how-to-use-group-by-and-partition-by-in-sql-f3d241846e3e
-
towardsdatascience.com/guide-to-sql-and-its-equivalent-commands-in-python-445e134adaba
-
medium.com/geekculture/date-time-manipulations-in-sql-d93d44bac723
-
towardsdatascience.com/distinguish-4-ranking-functions-in-sql-37db99107c05