TowardsDataScience 2023 博客中文翻译(三百二十三)

原文:TowardsDataScience

协议:CC BY-NC-SA 4.0

你 NLP 模型的终极预处理流程

原文:towardsdatascience.com/the-ultimate-preprocessing-pipeline-for-your-nlp-models-80afd92650fe

通过提供最佳可能的输入,充分发挥 NLP 机器学习模型的效果。

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

·发表在 Towards Data Science ·阅读时间 10 分钟·2023 年 5 月 8 日

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

照片由 Cyrus Crossan 提供,发布在 Unsplash 上。

如果你曾经参与过文本摘要项目,你可能会发现很难看到你期望的结果。你脑海中有一个关于算法如何工作的概念,以及它应该在文本摘要中标记哪些句子,但往往算法提供的结果是“不是很准确”的。更有趣的是关键词提取,因为从主题建模到向量化嵌入的各种算法都表现得很出色,但当输入一个段落时,它们给出的结果同样是“不是很准确”的,因为出现频率最高的词不一定是段落中最重要的词。

数据预处理和清洗的需求在很大程度上取决于你要解决的具体问题。我将尝试创建一个通用的流程,应该适用于所有 NLP 模型,但你总是需要调整这些步骤,以获得最佳的结果。在这个故事中,我将重点关注解决 主题建模、关键词提取和文本摘要 的 NLP 模型。

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

预处理流程 | 作者图像

上面的图片概述了我们将要遵循的预处理 NLP 管道的过程。上述四个步骤将在后面用代码解释,同时还附有一个 Jupyter 笔记本,实现了完整的管道。这条管道的想法是突出那些能够提升将在文本数据上使用的机器学习算法性能的步骤。这是在输入数据和模型训练之间的一个步骤。

1. 清理文本

结构化管道的第一步是清理输入文本数据,这可能包含几个步骤,具体取决于你试图构建的模型以及你期望的结果。机器学习算法(或者说几乎所有计算机算法,实际上是每一条计算机指令)都在数字上工作,这就是为什么为文本数据构建模型具有挑战性的原因。你实际上是在要求计算机学习和处理它从未见过的内容,因此需要多花一些功夫。

在下面的部分中,我提供了我们管道的第一个功能来执行文本数据的清理。清理功能包含许多操作部分,我在代码的注释中解释了它们。

要查看该功能的性能,下面是函数的输入和它生成的输出。

input_text = "This is an example from a key football match tweet text with \n
a <b>HTML tag</b>, an emoji 😃 expression happiness and 😍 with eyes too, we 
also have a link https://example.google.com, extra   w. h.  i.  t. e.   
spaces, accented characters like café, contractions we generally observe 
like don't and won't, some very special characters like @ and #, UPPERCASE 
letters, numericals like 123455, and general english stopwords like a, an, 
and the. Why not add punctuations like !, ?, and ,. too"

clean_text = clean_text(input_text)
print(clean_text)

----------------------------------------------------------------------------
example key football match tweet text html tag emoji grinning face big eyes 
expression happiness smiling face hearteyes eyes also link extra w h e spaces 
accented characters like cafe contractions generally observe like special 
characters like uppercase letters numericals like one hundred twentythree 
thousand  four hundred fiftyfive general english stopwords like add 
punctuations like

正如我们在输出中所观察到的,文本现在已经清除了所有的 HTML 标签,将表情符号转换为其文字形式,并纠正了任何标点符号和特殊字符。现在处理这些文本更为简便,在接下来的几步中,我们将进一步优化它。

2. 聚类以去除数据中的噪声和模板语言

我们预处理管道中的下一步可能是最重要且被低估的 NLP 工作流程活动。在下面的图示中,你可以看到下面的算法将要执行的粗略表示。

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

噪声去除过程的表示 | 作者提供的图片

那么,为什么去除噪声如此重要?因为这些文本隐藏在输入数据中,但不包含任何能够使学习算法更好的有用信息。像法律协议、新闻文章、政府合同等文件中包含了大量特定于组织的模板文本。假设你从法律合同中创建一个主题建模项目,以了解一系列合同中最重要的术语,而算法将管辖权解释和州法律定义作为合同中最重要的部分。法律合同包含了大量的法律和仲裁定义,但这些都是公开的,因此不特定于手头的合同,这使得这些预测实际上毫无用处。我们需要提取特定于该合同的信息。

从文本数据中去除模板语言是一项具有挑战性的工作,但却极为重要。由于这些数据都是干净的文本,因此很难检测和去除。但如果不去除,它会显著影响模型的学习过程。

现在让我们看看一个去除输入噪声和模板语言的函数的实现。这个算法使用聚类来找出重复出现的句子和词汇,并将其删除,假设重复超过阈值次数的内容可能是“噪声”。

下面,我们来看看这个函数在作为算法输入的新闻文章 [3] 上会产生什么结果。

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

去除模板代码的输出 | 图片由作者提供

从上面的输出图片中你可以看到,输入算法的文本长度为 7574,通过去除噪声和模板文本减少到 892。去除模板和噪声将我们的输入大小减少了近 88%, 这些原本会进入机器学习算法的垃圾数据。结果文本是更清晰、更有意义的摘要形式。通过去除噪声,我们让算法专注于重要的内容。

3. 词性标注

词性标注(POS),即为输入句子的每个词分配特定的词性标签的过程。它读取并理解词与句子中其他词的关系,并识别每个词的使用上下文。这些词性标签包括名词、动词、形容词、代词、介词、副词、连词和感叹词。这个过程至关重要,因为对于情感分析、文本分类、信息提取、机器翻译或任何其他形式的分析算法来说,了解词汇的使用上下文是非常重要的。上下文可以大大影响算法的自然语言理解(NLU)过程。

接下来,我们将进入预处理管道的最后一步,即将文本转换为向量嵌入,供机器学习算法使用。但是,在这之前,让我们讨论两个关键话题:词形还原和词干提取。

你需要词形还原(Lemmatization)还是词干提取(Stemming)?

词形还原和词干提取是自然语言处理工作流中常用的两种技术,帮助将屈折变化的词汇还原到其基础或根本形式。这些操作可能是最受质疑的,因此了解何时使用和何时不使用这些功能是值得的。词形还原和词干提取的核心思想是减少输入特征空间的维度。这有助于提高最终读取这些数据的机器学习模型的性能。

词干提取通过去除单词的后缀来将其还原为基本形式,而词形还原则使用词汇表和形态分析的形式将单词还原为基本形式。

由于其功能,词形还原通常比词干提取更准确,但计算开销较大。你特定用例中速度和准确性的权衡通常有助于决定使用哪种方法。

实施词形还原和词干提取时需要注意的一些重要点:

  1. 词形还原保留了输入文本的语义。针对情感分析的算法,如果模型需要单词的时态,可能会工作得很好。过去发生的事情可能与现在发生的相同事情具有不同的情感。

  2. 词干提取速度快,但准确性较低。在尝试进行文本分类的情况下,其中有成千上万的单词需要分类时,词干提取可能比词形还原更有效,主要是因为其速度。

  3. 像所有方法一样,可能值得在你的用例中探索两者,并比较模型的性能,以查看哪种效果最好。

  4. 此外,一些深度学习模型具备自动学习词汇表示的能力,这使得使用这些技术变得无关紧要。

4. 词形还原和向量嵌入

该预处理工作流程的最终步骤是应用词形还原和将单词转换为向量嵌入(因为要记住机器如何最好地处理数字而非单词吗?)。如前所述,词形还原可能根据你预期的结果和使用的机器学习技术,可能需要或不需要。为了更通用的方法,我在我的预处理管道中包含了它。

下面编写的函数将从接收到的词性标注输入中提取单词,对每个单词进行词形还原,然后将向量嵌入应用于词形还原后的单词。注释进一步解释了每个步骤。

该函数将返回一个形状为 (num_words, X) 的 numpy 数组,其中 ‘num_words’ 表示输入文本中的单词数量,‘X’ 是向量嵌入的大小。

向量嵌入的单词(单词的数值形式)应作为输入提供给任何机器学习算法。在使用深度学习模型或多个大型语言模型(LLMs)时,向量嵌入和词形还原可能不需要,因为算法已经足够成熟,可以构建自己对单词的表示。因此,如果你使用这些“自学习”算法中的任何一种,这可以是一个可选步骤。

完整管道实现

上述四个部分详细说明了我们预处理管道中的每个部分,下面附有用于运行预处理代码的工作笔记本。

[## ai-with-python-series/NLP 预处理管道

一系列旨在学习人工智能概念的 Python 教程。该系列教程从…

github.com

我想提醒您,这种实现并不是对每个 NLP 问题的一次性解决方案。构建一个稳健的预处理管道的核心思想是创建一个能够将最佳输入提供给机器学习算法的工作流程。上述步骤的顺序应该能解决约 70%的问题,通过针对特定用例的微调,您应该能够解决剩余部分。

结论

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

Brett Jordan 拍摄,来自 Unsplash

我希望这个故事为您下一个自然语言处理(NLP)项目提供了一个稳健的框架。虽然处理词语是 AI 领域中最具挑战性的任务之一,但数据的适当清理、预处理和准备可以确保机器的学习过程顺畅。正如正确实施本文提到的技术一样,遵循这里突出显示的预处理活动的顺序同样重要

每一步都依赖于前一步的成功。

如果输入到该算法中的数据包含大量停用词或 HTML 标签,模板删除逻辑将无法准确识别噪音。类似地,对整个文本进行词性标注、词形还原和向量化会使计算成本极高,并且很少像垃圾进垃圾出那样有效。因此,请尝试这个管道,并在您的 NLP 设置中享受更好的结果。

关于我

我已经使用 AI 构建产品和解决方案好几年了。在我目前在 Bloomberg 的职位之前,我曾在 Microsoft、Tesla 和 Johnson & Johnson 从事数据和机器学习工作。我拥有哥伦比亚大学的数据科学学位,同时也参与了人工智能负责任和伦理使用的研究。除了我的工作,我还是两本关于机器学习和数据科学的书籍及在线课程的作者。我不断探索通过利用 AI 解决复杂问题的方式,以在世界上产生积极影响,同时秉持伦理和负责任的实践。

随时通过LinkedInTwitter与我联系。

这里有更多你可能感兴趣的数据科学内容!

## 自然语言处理(NLP)的详细新手入门

《Python 中开始使用 NLP 的终极代码指南》

towardsdatascience.com ## Python 中语音识别和音频信号处理的逐步指南

教授机器人类词汇的科学

towardsdatascience.com ## 使用 Python 从面部表情识别情感的终极指南

构建一个可以处理图像和视频的面部情感识别(FER)算法

towardsdatascience.com

资源

[1] dataaspirant.com/nlp-text-preprocessing-techniques-implementation-python/#t-1600081660732

[2] www.exxactcorp.com/blog/Deep-Learning/text-preprocessing-methods-for-deep-learning

[3] www.nbcnews.com/news/us-news/oklahoma-sex-offender-suspected-killing-6-was-free-sex-crimes-charges-rcna82884

[4] www.nltk.org/book/ch05.html

[5] www.turing.com/kb/guide-on-word-embeddings-in-nlp

Apple Silicon Macs(M1 和 M2)的终极 Python 和 Tensorflow 设置指南

原文:towardsdatascience.com/the-ultimate-python-and-tensorflow-set-up-guide-for-apple-silicon-macs-m1-m2-e9ef304a2c06

逐步指南

在 ARM Mac 上安装 TensorFlow 和 Python 从未如此简单

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

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

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

照片由 Ales Nesetril 提供,来源于 Unsplash

一年前我换了工作,发现自己拥有一台全新的 M1 MacBook Pro。我需要安装数据科学家所需的一切,这确实是一件痛苦的事。

更糟糕的是,事实证明我没有正确设置,一片混乱。

但一位同事分享了一套步骤,在按照这些步骤成功设置之后,我想把它分享给大家。所以我总结并扩展了额外的信息和步骤,以确保完整性。因为我在网上找不到类似的内容,至少在我搜索时没有找到,所以我决定公开它,以便大家都能受益。

在这个故事中,你会找到一个逐步指南,讲解如何在 M1 和 M2 Mac 上成功安装 Python 和 Tensorflow,而无需自己费力去设置。

工作流程相对简单:

  1. 我们将首先安装任何开发者在 Mac 上需要的基本要求(如果你不是在使用新的 Mac,可能这些要求已经满足了)。

  2. 然后,使用pyenv进行 Python 的安装。

  3. 最后,正确安装和设置 Tensorflow 以适用于 M1 或 M2 Mac。

你只需要一台 ARM Mac,就可以开始了!

设置 Python

现在的 Mac 通常已经预装了 Python,至少是 Python2,但我相信在像 M1 或 M2 MacBook 这样的 arm64 设备上,有更好和推荐的 Python 使用方式。

XCode 命令行工具

首先,你需要安装 XCode 命令行工具:

~> xcode-select --install
~> xcrun --show-sdk-path
/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk

这些工具是为在命令行中运行的软件开发人员准备的,存在于终端应用程序中。自从苹果公司成立之前,这些基于 Unix 的工具组合就已经是几乎所有软件开发的基础。

Homebrews

一旦我们安装了命令行工具,我们需要安装两个 homebrews。为什么是两个?我们想要一个本地(Apple Silicon)和一个 Intel(也叫 Rosetta)。为此,我们需要为每种架构准备一个 Terminal/iTerm2。很简单。

我推荐使用 iTerm2[1],因为它是改进版的 macOS 终端,但如果你更喜欢使用内置终端,它的功能是相同的。

转到你的应用程序文件夹,复制“iTerm”并将副本重命名为“intel iTerm”(或类似名称)。对于那个新应用程序,右键点击并选择“获取信息”。在常规部分,勾选“使用 Rosetta 打开”。

现在我们需要设置 Homebrew [2]。

从 Intel 终端运行:

~> arch --x86_64 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

如果你不知道自己使用的是哪个终端,可以使用uname -m并查看输出:

  • 如果你看到x86_64,那么你在 Intel/Rosetta 终端中。

  • 但如果输出显示arm64,则说明你在本地终端(M1/M2)中。

现在打开本地终端(默认的 iTerm 或 Terminal),并运行下一组指令:

~> cd ~/Downloads
~> mkdir homebrew
~> curl -L https://github.com/Homebrew/brew/tarball/master | tar xz --strip 1 -C homebrew
~> sudo mv homebrew /opt/homebrew

现在检查两个终端中是否都已正确安装。

  • 在 Intel iTerm 上:
~> brew --prefix
/usr/local
~> brew list
<list of installed packages>
  • 在本地 iTerm 上:
~> brew --prefix
/opt/homebrew
~> brew list
<list of installed packages (different than x86_64)>

现在,为了正确管理这两个环境,我们需要进行适当的设置。只需在你的~/.zshrc文件中添加以下配置:

... rest of the file above ...

#
# homebrew setup, following https://noahpeeters.de/posts/apple-silicon/homebrew-setup/
#
if [ -d "/opt/homebrew/bin" ]; then
    export PATH="/opt/homebrew/bin:$PATH"
fi

function ibrew() {
   arch --x86_64 /usr/local/bin/brew $@
}

# variables needed to properly install things under intel or m1

ARCH="$(uname -m)"
case ARCH in
    i386)   ARCH="386" ;;
    i686)   ARCH="386" ;;
    x86_64) ARCH="amd64" ;;
    arm)    dpkg --print-architecture | grep -q "arm64" && ARCH="arm64" || ARCH="arm" ;;
esac

if [[ "${ARCH}"  == "arm64" ]]; then
    PREFIX="/opt/homebrew"
else
    PREFIX="/usr/local"
fi

# https://github.com/pyenv/pyenv/issues/1768
SDK_PATH="$(xcrun --show-sdk-path)"

echo $PREFIX
echo $SDK_PATH

export CPATH="${SDK_PATH}/usr/include"
export CFLAGS="-I${SDK_PATH}/usr/include/sasl $CFLAGS"
export CFLAGS="-I${SDK_PATH}/usr/include $CFLAGS"
export CFLAGS="-I${PREFIX}/include $CFLAGS"
export LDFLAGS="-L${SDK_PATH}/usr/lib $LDFLAGS"
export LDFLAGS="-L${PREFIX}/lib $LDFLAGS"
export LDFLAGS="-L${PREFIX}/opt/openssl/lib $LDFLAGS"
export CPPFLAGS="-I${PREFIX}/opt/openssl/include $CPPFLAGS"

为了让 brew 正常工作,只需要第一个部分……但是后面添加的其他标志可能也会有用(可能需要?)。

记录一下:我们会尽量首先使用本地 brew 来安装包。只有当某个包安装失败且确认本地版本无法安装时,我们才会使用 Intel brew。

Pyenv

好的,现在我们已经可以开始安装与 Python 相关的内容了。我们将从pyenv开始,这将使安装各种 Python 版本的过程极其顺利。

打开你的本地终端,然后简单地输入下面的命令:

~> brew install pyenv

构建依赖

为了让 pyenv 正常工作,我们需要安装几个依赖项[3]。在我们的例子中:

~> brew install openssl readline sqlite3 xz zlib tcl-tk libffi

此外,我们还希望在我们的 intel homebrew 中添加相同的依赖项,所以:

~> ibrew install openssl readline sqlite3 xz zlib tcl-tk libffi

现在,你可以开始安装 Python 版本了。下面是一个示例,如果你想安装 Python 3.9.9 并将其设置为默认版本:

~> pyenv install 3.9.9
~> pyenv global 3.9.9

如果你想查看当前安装的版本,只需使用pyenv versions

因为这种过程双重检查总是值得的,所以运行下一个命令:~/.pyenv/versions/3.9.9/bin/python,将3.9.9替换为你决定安装并设置为全局的版本。一旦进入 shell,尝试导入ctypes

Python 3.9.9 (main, Feb 15 2023, 11:25:42)
[Clang 14.0.0 (clang-1400.0.29.202)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import ctypes
>>>

如果它没有中断,那么它就已正确安装。

Pipenv

虽然安装它不是强制性的,但为你的项目使用环境始终是一个好习惯。这也是我决定在这里提到它的原因,因为它简单且好用。安装方法如下:

~> brew install pipenv

现在你可以在任何项目中使用它。你可以查看 pipenv 的基本用法,访问他们的官方文档[4]或寻找在线教程。

安装 Tensorflow

现在进入第二部分。安装 TensorFlow 在 M1 或 M2 Mac 上曾让我们许多人感到痛苦,但现在不必再这样了。

你会记得我们之前编辑了 ~/.zshrc 文件。现在,我们将在文件末尾添加这一行:

# tensorflow grpcio https://github.com/grpc/grpc/issues/25082
export GRPC_PYTHON_BUILD_SYSTEM_OPENSSL=1
export GRPC_PYTHON_BUILD_SYSTEM_ZLIB=1
export CFLAGS="-I/opt/homebrew/opt/openssl/include"
export LDFLAGS="-L/opt/homebrew/opt/openssl/lib"

一旦添加完成,我们需要将 miniconda 下载到 ~/ 目录。你可以直接从 repo.anaconda.com/miniconda/Miniconda3-latest-MacOSX-arm64.sh 下载,并手动移动到主文件夹。

下载完成后,我建议退出并重新打开原生终端,然后运行:

~> bash ~/Miniconda3-latest-MacOSX-arm64.sh -b -p $HOME/miniconda

现在我们已经安装了 conda,我们将激活环境并安装一些 Tensorflow 的依赖项:

~> source ~/miniconda/bin/activate
(base) ~> conda install -c apple tensorflow-deps

再次,通过退出(Cmd + Q)并重新打开终端,你现在可以安装 Tensorflow。我将以这种方式在专用环境中进行安装:

  1. 进入你的项目文件夹:例如 cd Documents/project

  2. 激活环境:pipenv shell

  3. 安装 Tensorflow:pipenv install tensorflow-macos

瞧好了! 你现在应该能在 M1 或 M2 Mac 上正确使用 TensorFlow 了。

看看我安装了一个名为 tensorflow-metal[5] 的包来加速我们 Mac 上 GPU 模型的训练,所以你可以考虑用

~> pipenv install tensorflow-metal

结论

现在你知道如何应对设置全新 Mac 用于数据科学的痛苦,并充分利用其新芯片。

希望这些信息对你有所帮助,如果你遇到任何疑问或问题,请随时在这个故事下评论——我或其他专家一定会帮助你。

我不想离开而不感谢你阅读我的故事。我经常在 Medium 上发帖,如果你喜欢这篇文章,欢迎关注我。这对我很有帮助。

如果你想进一步支持我,可以通过下面的链接订阅 Medium 会员:这不会让你多花一分钱,但它会帮助我度过这个过程。非常感谢!

[## 通过我的推荐链接加入 Medium — Pol Marin

阅读 Pol Marin 的所有故事(以及 Medium 上其他成千上万的作家)。你的会员费直接支持 Pol…

medium.com](https://medium.com/@polmarin/membership?source=post_page-----e9ef304a2c06--------------------------------)

资源

[1] iTerm2 — macOS 终端替代品

[2] Noah Peters, 在 Apple Silicon 上设置 Homebrew

[3] Pyenv 维基

[4] Pipenv 的基础用法

[5] 官方 PyPi — Tensorflow-metal 包

终极可视化助手

原文:towardsdatascience.com/the-ultimate-visualization-assistant-2dc89003d847

一个与 AI 的夜晚如何改变了我对数据可视化的方式

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

·发表于 Towards Data Science ·阅读时间 9 分钟·2023 年 6 月 30 日

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

Simon Abrams的照片,来源于Unsplash

当太阳开始暗淡,城市灯光逐渐亮起时,办公室熬夜的不可避免性开始显现。我发现自己在与时间赛跑。一个关键的销售演示即将到来,而成功取决于一个未完成的请求——一个能够简洁传达我们新电视广告归因模型基本方法的有洞察力的数据可视化。

我需要一种既足够复杂以展示,又足够简单以无需解释即可传达大部分信息的东西。在时间紧迫的情况下,这并不容易。值得庆幸的是,人工智能的灯塔,我可靠的新助手——ChatGPT——随时准备简化眼前的任务。它的主要使命是:帮助我将脑海中仅仅是概念的数据转化为优雅的视觉叙事,能够吸引即使是最厌恶数据的观众。

这可能有点戏剧化,但我确实有点赶时间。对于一个销售演示,这需要一个过于简单的例子,我在我们的数据库中找不到,因此只好使用虚构的示例数据。

我脑海中清楚地知道我想展示的内容——一个网络流量基线、电视广告后网络响应的激增,以及一种在几乎没有文字的情况下传达我们将在这个例子中将基线与激增之间的差异归因的方式。

Matplotlib 图表在学术论文中非常扎实,但制作出既复杂又真正引人注目的图表可能需要过人的技巧。我经常转向 Power BI 来获取现成的外观和快速图表,但当你需要从零开始制定某些内容而没有现成数据时,它并不理想。

我已经使用了 Plotly 一段时间,这似乎是一个不错的选择。然而,我对这个包的信心不够,不能在不花费宝贵时间翻阅文档和 Stack Overflow 的情况下把我的想法变成代码。因此,时间紧迫,我开始了我的第一个提示。

我正在创建一个可视化图表,显示电视广告播出后的网络流量反应。我们从将网络流量表示为 30 分钟内每分钟网站访问次数开始。前十分钟需要是 0 到 4 之间的随机数。第二个十分钟周期需要是 4 到 20 之间的随机数,并且这些数字分布看起来像右偏直方图。第三个十分钟周期与第一个十分钟周期相同。请使用 Plotly 创建这些数据并绘制折线图。

提示返回了以下内容,我仅在 update_layout 函数中进行了编辑:

import numpy as np
import plotly.graph_objects as go

# Set seed for reproducibility
np.random.seed(42)

# Create arrays with the specified distributions
first_ten = np.random.uniform(low=0, high=4, size=10)
last_ten = np.random.uniform(low=0, high=4, size=10)
middle_ten = np.random.gamma(shape=2, scale=2, size=10) + 4 # shift the gamma distribution

# Combine the arrays
measurements = np.concatenate((first_ten, middle_ten, last_ten))

# Create x values
x_values = np.arange(1, 31)

# Create the line chart
fig = go.Figure(
    data=go.Scatter(
        x=x_values, 
        y=measurements, 
        mode='lines+markers')
    )

fig.update_layout(
    title='Ad Influence on Web Traffic', 
    xaxis_title='Time', 
    yaxis_title='Visits per Minute',
    width=1000,
    height=600
)

fig.show()

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

上一个代码块生成的图表

我已经对这一点感到印象深刻,因为我没想到右偏直方图样式的请求处理得如此出色。也许我甚至能在下班前的聚会结束之前完成。

接下来,我需要一些东西来表示网络流量的基线。实际上,这可以简单到滚动中位数,但由于数据长度有限,这里要求这个可能行不通。相反,我认为前十组和后三组十个观测值的中位数可能更能传达正确的想法。

请首先计算两个数组 first_ten 和 last_ten 的联合中位数。将计算得到的中位数绘制为常量水平线。

import numpy as np
import plotly.graph_objects as go

# Set seed for reproducibility
np.random.seed(42)

# Create arrays with the specified distributions
first_ten = np.random.uniform(low=0, high=4, size=10)
last_ten = np.random.uniform(low=0, high=4, size=10)
middle_ten = np.random.gamma(shape=2, scale=2, size=10) + 4 # shift the gamma distribution

# Combine the arrays
measurements = np.concatenate((first_ten, middle_ten, last_ten))

# Create x values
x_values = np.arange(1, 31)

# Calculate combined median
combined_median = np.median(np.concatenate((first_ten, last_ten)))

# Create the line chart
fig = go.Figure()

# Create the line chart
fig = go.Figure(
    data=go.Scatter(
        x=x_values, 
        y=measurements, 
        mode='lines+markers')
    )

# Add median line
fig.add_trace(
    go.Scatter(
        x=x_values, 
        y=[combined_median]*len(x_values), 
        mode='lines', 
        name='Baseline', 
        line=dict(color='firebrick', width=2, dash='dash'))
)

fig.update_layout(
    title='Ad Influence on Web Traffic',
    xaxis_title='Time',
    yaxis_title='Visits per Minute',
    width=1000,
    height=600
)

fig.show()

然后我仅将中位数线追踪的名称从“Median”更改为“Baseline”。

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

上面代码块中的图表,添加了基线常量线

现在,如果你对营销模型有所了解,“提升”的概念就会显现出来,但对于未受过训练的眼睛来说,这需要一些更集中的元素;需要解释的越少,视觉效果就越有效。此时主要缺少的是一个能暗示网络流量激增的因果关系的图表元素,以及一种传达归因于广告的流量量的方法。

我从因果关系元素开始。

在数据的第 10 分钟,一则电视广告播出,并引发了网络流量的激增。你会建议使用什么样的图表元素来传达不仅广告在那一分钟播出,而且广告是流量激增的原因?

响应建议使用垂直线和注释,并提供了代码更新。

import numpy as np
import plotly.graph_objects as go

# Set seed for reproducibility
np.random.seed(42)

# Create arrays with the specified distributions
first_ten = np.random.uniform(low=0, high=4, size=10)
last_ten = np.random.uniform(low=0, high=4, size=10)
middle_ten = np.random.gamma(shape=2, scale=2, size=10) + 4 # shift the gamma distribution

# Combine the arrays
measurements = np.concatenate((first_ten, middle_ten, last_ten))

# Create x values
x_values = np.arange(1, 31)

# Calculate combined median
combined_median = np.median(np.concatenate((first_ten, last_ten)))

# Create the line chart
fig = go.Figure()

# Add scatter plot
fig.add_trace(
    go.Scatter(
        x=x_values, 
        y=measurements, 
        mode='lines+markers', 
        name='Web Traffic')
)

# Add median line
fig.add_trace(
    go.Scatter(
        x=x_values, 
        y=[combined_median]*len(x_values), 
        mode='lines', 
        name='Baseline', 
        line=dict(color='firebrick', width=2, dash='dash'))
)

# Add vertical line at minute 10
fig.add_shape(
    type='line',
    x0=10, y0=0,
    x1=10, y1=max(measurements),
    line=dict(
        color="Purple",
        width=3,
        dash="dot",
    )
)

# Add annotation to indicate TV spot
fig.add_annotation(
    x=10,
    y=max(measurements),
    text="TV ad aired",
    showarrow=True,
    arrowhead=1,
    ax=-100,
    ay=-25,
    font=dict(
        size=16,
        color="Black"
    ),
)

fig.update_layout(
    title='Ad Influence on Web Traffic',
    xaxis_title='Time',
    yaxis_title='Visits per Minute',
    width=1000,
    height=600
)

fig.show()

我仅更改了注释的字体大小。

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

上面代码块中的图表,添加了电视广告播出的常量线和注释

现在我们有了明确的原因和发生点来解释峰值。图表仍然需要一些东西来表示归因的流量。我已经知道我想要的——为“提升窗口”(十分钟)期间的基线和网络流量之间的区域添加阴影。我回到我的 AI 同事那里实现这个。

我想显示归因于广告的基线以上的网络流量。请在基线线和每分钟网络流量线之间添加绿色阴影。只为数据的第 10 分钟到第 20 分钟之间的区域着色。还请为阴影区域添加图例项。

import numpy as np
import plotly.graph_objects as go

# Set seed for reproducibility
np.random.seed(42)

# Create arrays with the specified distributions
first_ten = np.random.uniform(low=0, high=4, size=10)
last_ten = np.random.uniform(low=0, high=4, size=10)
middle_ten = np.random.gamma(shape=2, scale=2, size=10) + 4 # shift the gamma distribution

# Combine the arrays
measurements = np.concatenate((first_ten, middle_ten, last_ten))

# Create x values
x_values = np.arange(1, 31)

# Calculate combined median
combined_median = np.median(np.concatenate((first_ten, last_ten)))

# Create the line chart
fig = go.Figure()

# Add scatter plot
fig.add_trace(
    go.Scatter(
        x=x_values, 
        y=measurements, 
        mode='lines+markers', 
        name='Web Traffic')
)

# Add median line
fig.add_trace(
    go.Scatter(
        x=x_values, 
        y=[combined_median]*len(x_values), 
        mode='lines', 
        name='Baseline', 
        line=dict(color='firebrick', width=2, dash='dash'))
)

# Add shaded region for the influence of the ad
fig.add_trace(
    go.Scatter(
        x=list(x_values[9:20]) + list(x_values[9:20])[::-1], 
        y=list([combined_median]*11) + list(measurements[9:20])[::-1], 
        fill='toself', 
        fillcolor='rgba(0,200,0,0.2)', 
        line=dict(width=0), 
        hoverinfo="skip", 
        showlegend=True, 
        name='Ad Impact')
)

# Add vertical line at minute 10
fig.add_shape(
    type='line',
    x0=10, y0=0,
    x1=10, y1=max(measurements),
    line=dict(
        color="Purple",
        width=3,
        dash="dot",
    )
)

# Add annotation to indicate TV spot
fig.add_annotation(
    x=10,
    y=max(measurements),
    text="TV ad aired",
    showarrow=True,
    arrowhead=1,
    ax=-100,
    ay=-25,
    font=dict(
        size=16,
        color="Black"
    ),
)

fig.update_layout(
    title='Ad Influence on Web Traffic',
    xaxis_title='Time',
    yaxis_title='Visits per Minute',
    width=1000,
    height=600
)

fig.show()

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

来自上述代码块的图表,添加了表示广告影响的阴影。

我感觉这已经到了一个不错的地方,但对我试图传达的“清晰度”仍不满意。我根据已经提供的代码做了一些补充。

我添加了一条常量垂直线和注释以标示提升窗口的截断,并将广告播放线和窗口截断线的颜色分别更改为绿色和红色。我还将基线常量线更改为紫色,并将其样式设置为虚线,以更好地匹配其他常量线。

然后我想要一个最终元素——基线区域的阴影。希望继续保持迄今为止的完美表现,我把这交给了我的助理。

请对整个基线区域添加紫色阴影。在第 15 分钟的基线水平线上方添加注释“Baseline”。

注意,我在这里没有引用任何坐标或阴影位置的具体数字。

import numpy as np
import plotly.graph_objects as go

# Set seed for reproducibility
np.random.seed(42)

# Create arrays with the specified distributions
first_ten = np.random.uniform(low=0, high=4, size=10)
last_ten = np.random.uniform(low=0, high=4, size=10)
middle_ten = np.random.gamma(shape=2, scale=2, size=10) + 4 # shift the gamma distribution

# Combine the arrays
measurements = np.concatenate((first_ten, middle_ten, last_ten))

# Create x values
x_values = np.arange(1, 31)

# Calculate combined median
combined_median = np.median(np.concatenate((first_ten, last_ten)))

# Create the line chart
fig = go.Figure()

# Add baseline region
fig.add_trace(
    go.Scatter(
        x=list(x_values) + list(x_values)[::-1], 
        y=[combined_median]*len(x_values) + [0]*len(x_values), 
        fill='toself', 
        fillcolor='rgba(128, 0, 128, 0.2)', 
        line=dict(width=0), 
        hoverinfo="skip", 
        showlegend=True, 
        name='Baseline')
)

# Add scatter plot
fig.add_trace(
    go.Scatter(
        x=x_values, 
        y=measurements, 
        mode='lines+markers', 
        name='Web Traffic',
        line=dict(color='#636EFA'))
)

# Add median line
fig.add_trace(
    go.Scatter(
        x=x_values, 
        y=[combined_median]*len(x_values), 
        mode='lines', 
        name='Median', 
        line=dict(color='Purple', width=2, dash='dash'))
)

# Add shaded region for the influence of the ad
fig.add_trace(
    go.Scatter(
        x=list(x_values[9:20]) + list(x_values[9:20])[::-1], 
        y=list([combined_median]*11) + list(measurements[9:20])[::-1], 
        fill='toself', 
        fillcolor='rgba(0,200,0,0.2)', 
        line=dict(width=0), 
        hoverinfo="skip", 
        showlegend=True, 
        name='Ad Impact')
)

# Add vertical line at minute 10
fig.add_shape(
    type='line',
    x0=10, y0=0,
    x1=10, y1=max(measurements),
    line=dict(
        color="Green",
        width=3,
        dash="dot",
    )
)

# Add annotation to indicate TV spot
fig.add_annotation(
    x=10,
    y=max(measurements),
    text="TV ad aired",
    showarrow=True,
    arrowhead=1,
    ax=-100,
    ay=-25,
    font=dict(
        size=16,
        color="Green"
    ),
)

# Add vertical line at minute 20
fig.add_shape(
    type='line',
    x0=20, y0=0,
    x1=20, y1=max(measurements),
    line=dict(
        color="Red",
        width=3,
        dash="dot",
    )
)

# Add annotation to indicate lift window cutoff
fig.add_annotation(
    x=20,
    y=max(measurements),
    text="Lift window cutoff",
    showarrow=True,
    arrowhead=1,
    ax=100,
    ay=-25,
    font=dict(
        size=16,
        color="Red"
    ),
)

# Add annotation for lift
fig.add_annotation(
    x=15,
    y=combined_median + (max(measurements[9:20])-combined_median)/4,
    text="Lift due to Ad",
    showarrow=False,
    font=dict(
        size=18,
        color="Green"
    ),
)

# Add annotation for baseline
fig.add_annotation(
    x=15,
    y=combined_median/2,
    text="Baseline",
    showarrow=False,
    font=dict(
        size=18,
        color="Purple"
    ),
)

fig.update_layout(
    title='Ad Influence on Web Traffic',
    xaxis_title='Time',
    yaxis_title='Visits per Minute',
    width=1000,
    height=600
)

fig.show()

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

来自上述代码块的图表,添加了基线阴影、截断线和更多注释。

基线阴影请求的捕捉没有提供具体值是顶级的,这一点我已经深感印象。对可视化效果满意后,我将图表放入演示文稿中,发了一条 Teams 消息,并怀着依依不舍的目光看了下 ChatGPT 标签,关闭了我的笔记本电脑。当我收拾东西时,来自下班后群聊的通知在我的手机屏幕上闪烁。

看了演示消息,觉得你说你会晚点来?我们会为你准备一品脱啤酒。

祝好,ChatGPT。

《被低估的珍宝 第一部分:8 个让你成为高手的 Pandas 方法》

原文:towardsdatascience.com/the-underrated-gems-pt-1-8-pandas-methods-that-will-make-you-a-pro-b1437a19ca3c?source=collection_archive---------0-----------------------#2023-07-17

被低估的、未被重视的和未被深入探索的

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

·

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

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

图片由 Roman Kraft 提供,来源于 Unsplash

“在喧嚣的环境中,正是那些低声细语的话语才蕴藏着隐藏的智慧 💎”

暂时忘记 ChatGPT。对我们中的一些人来说,我们会因为每次想执行简单的 Pandas 操作时都要不断地谷歌搜索解决方案而感到疲惫。似乎有无数种方法来做同一件事,那么究竟哪一种才是最佳选择?虽然有很多可能的解决方案确实很好,但也带来了不一致和对代码功能的困惑。

到达罗马有 1000 条可能的路线,可能甚至更多。问题是,你选择隐藏的小路还是复杂的路线?

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

图片由作者提供

这里是本文的要点。我将带你了解如何通过处理来自UCI 机器学习的自行车共享数据集¹来将这些方法付诸实践。通过采用这些方法,你不仅可以简化数据处理代码,还能更深入地理解你编写的代码。让我们开始导入数据集,并快速查看 DataFrame!

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

bike = (pd
        .read_csv("../../dataset/bike_sharing/day.csv")
       )
bike

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

图片由作者提供

目录

  • 方法 #1: **.assign()**

  • 方法 #2: **.groupby()**

  • 方法 #3: **.agg()**

  • 方法 #4: **.transform()**

  • 方法 #5: **.pivot_table()**

  • 方法 #6: **.resample()**

  • 方法 #7: **.unstack()**

  • 方法 #8: **.pipe()**

☕️ 方法 #1: .assign()

忘掉使用**df["new_col"] =****df.new_col =**这种操作来创建新列。这里是你应该使用**.assign()**方法的原因——它返回一个 DataFrame 对象,允许你继续链式操作以进一步处理 DataFrame。与**.assign()**方法不同,上述两种臭名昭著的操作会返回None,这意味着你无法进一步链式操作。

如果你不信,那么让我重新提起旧的敌人——**SettingWithCopyWarning**。相信我们每个人在某个时刻都遇到过这个问题。

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

图片由作者提供

警告够了,我不想在我的笔记本中再看到丑陋的红色框框!

使用**.assign()**,让我们添加一些新列,如**ratio_casual_registered****avg_temp****ratio_squared**

(bike
 .assign(ratio_casual_registered = bike.casual.div(bike.registered),
         avg_temp = bike.temp.add(bike.atemp).div(2),
         ratio_squared = lambda df_: df_.ratio_casual_registered.pow(2))
)

简而言之,上述方法的作用如下:

  1. 我们可以使用**.assign()**方法创建任意数量的新列,用逗号分隔。

  2. 在创建列**ratio_squared**时使用的 lambda 函数是为了访问我们在添加列**ratio_casual_registered**后最接近的 DataFrame。假设我们不使用 lambda 函数来访问最新的 DataFrame **df_**,而是继续使用**bike.ratio_casual_registered.pow(2)**,我们将会得到一个错误,因为原始 DataFrame 在创建**ratio_squared**之前,即使在**.assign()**方法中添加了该列,也没有**ratio_casual_registered**列。如果你对这个概念无法理解以决定是否使用 lambda 函数,我的建议是使用它!

  3. 额外的!我留下了一些不那么常见的使用方法进行算术运算。

☕️ 方法 #2: .groupby()

好吧,**.groupby()**方法虽然不罕见,但在我们深入探讨下一个方法之前,它们是必要的。一个常被忽视且未被提及的事情是,**.groupby()**方法具有懒惰特性。也就是说,该方法是延迟评估的。换句话说,它不会立即评估,这就是为什么你在调用**.groupby()**方法后常常会看到**<pandas.core.groupby.generic.DataFrameGroupBy object at 0x14fdc3610>**

从 Pandas DataFrame 文档² 中,传递给参数**by**的值可以是映射、函数、标签、pd.Grouper 或此类的列表。 然而,你最常遇到的情况可能是按列名分组(用逗号分隔的 Series 名称列表)。在**.groupby()**操作之后,我们可以执行诸如**.mean()****.median()**,或使用**.apply()**应用自定义函数等操作。

我们传递给**.groupby()**方法的**by**参数中的指定列的值将成为结果的索引。如果我们指定了多个列进行分组,那么我们将获得一个 MultiIndex。

(bike
 .groupby(['season', 'weathersit'])
 .mean(numeric_only=True) #alternative version: apply(lambda df_: df_.mean(numeric_only=True)) 
 .atemp
)

在这里,我们按照列**season****weathersit**对 DataFrame 进行了分组。然后,我们计算了均值,并仅选择了列**atemp**

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

图片来源于作者

☕️ 方法 #3: .agg()

如果你足够细心去深入研究 Pandas 文档²,你可能会遇到**.agg()****.aggregate()**这两种方法。你可能会想知道它们有什么区别,以及什么时候使用哪个?节省你的时间吧!它们是相同的,**.agg()**只是**.aggregate()**的别名。

**.agg()**有一个参数**func**,它可以接受函数、字符串函数名或函数列表。顺便提一下,你也可以在列上聚合不同的函数!让我们继续上面的例子!

#Example 1: Aggregating using more than 1 function
(bike
 .groupby(['season'])
 .agg(['mean', 'median'])
 .atemp
)

#Example 2: Aggregating using different function for different columns
(bike
 .groupby(['season'])
 .agg(Meann=('temp', 'mean'), Mediann=('atemp', np.median))
)

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

图片来源于作者

☕️ 方法 #4: .transform()

使用 **.agg()**,我们得到的结果在维度上相较于初始数据集是缩减的。简单来说,你的数据维度会减少,包含更少的行和列,包含汇总信息。如果你想对分组数据进行总结并获得聚合值,那么 **.groupby()** 是解决方案。

使用 **.transform()**,我们也开始时意图进行信息的汇总。然而,与创建信息汇总不同的是,我们希望输出的形状与原始 DataFrame 相同,而不缩小原始 DataFrame 的大小。

对于那些接触过类似 SQL 的数据库系统的人来说,**.transform()** 的概念可能与窗口函数类似。让我们看看 **.transform()** 在上面的示例中是如何工作的!

(bike
 .assign(mean_atemp_season = lambda df_: df_
                                          .groupby(['season'])
                                          .atemp
                                          .transform(np.mean, numeric_only=True))
)

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

图片由作者提供

如上所见,我们创建了一个名为 **mean_atemp_season** 的新列,并将该列填充为 **atemp** 列的汇总(均值)。因此,每当 **season** 为 1 时,**mean_atemp_season** 的值就相同。请注意这里的重要观察点是我们保留了原始数据集的维度,并增加了一列!

☕️ 方法 #5: .pivot_table()

这是给那些对 Microsoft Excel 迷恋的人的额外提示。你可能会被诱惑使用 **.pivot_table()** 来创建汇总表。这个方法当然也有效!但这里有一点建议,**.groupby()** 更加灵活,适用于更广泛的操作,不仅限于重新排列,还包括过滤、转换或应用组特定的计算。

简要说明一下如何使用 **.pivot_table()**。你在 **values** 参数中指定你要汇总的列。接下来,使用原始 DataFrame 的子集指定你想要创建的汇总表的 index。这可以是多列,汇总表将是 MultiIndex 的 DataFrame。接下来,使用未选择为索引的原始 DataFrame 的子集来指定你想要创建的汇总表的 columns。最后但同样重要的是,不要忘记指定 **aggfunc**!我们来快速看一下!

(bike
 .pivot_table(values=['temp', 'atemp'],
              index=['season'],
              columns=['workingday'],
              aggfunc=np.mean)
)

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

图片由作者提供

☕️ 方法 #6: .resample()

粗略地说,**.resample()** 方法可以被视为专门用于时间序列数据的分组和汇总,其中

DataFrame 或 Series 的索引是类似日期时间的对象。

这使你可以根据不同的时间频率对数据进行分组和汇总,例如每小时、每天、每周、每月等。更一般地说,**.resample()** 可以接受 DateOffset、Timedelta 或 str 作为规则来执行重采样。让我们将其应用于之前的示例。

def tweak_bike(bike: pd.DataFrame) -> pd.DataFrame:
    return (bike
            .drop(columns=['instant'])
            .assign(dteday=lambda df_: pd.to_datetime(df_.dteday))
            .set_index('dteday')
           )
bike = tweak_bike(bike)
(bike
 .resample('M')
 .temp
 .mean()
)

简而言之,我们在上面所做的操作是删除**instant**列,用**dteday**列覆盖**dteday**列,将**dteday**列的类型从object转换为**datetime64[ns]**类型,最后将这个**datetime64[ns]**列设为 DataFrame 的索引。

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

作者提供的图片

(bike
 .resample('M')
 .temp
 .mean()
)

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

作者提供的图片

在这里,我们获得了特征**temp**的描述性统计汇总(均值),以月频率呈现。尝试使用不同的频率,如**Q****2M****A**等,来玩转**.resample()**方法。

☕️ 方法 #7: .unstack()

我们快要结束了!让我给你展示一下**.unstack()**为何既强大又有用。在此之前,让我们回到上述一个例子,我们想通过使用**.groupby()****.agg()**来找到不同季节和天气情况的平均温度。

(bike
 .groupby(['season', 'weathersit'])
 .agg('mean')
 .temp
)

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

作者提供的图片

现在,让我们使用一个线图来可视化这一点,通过将**.plot****.line()**方法最小化地链接到上述代码中。在后台,Pandas 利用 Matplotlib 绘图后端来完成绘图任务。这给出了以下结果,我们都不想要,因为图的 x 轴是按 MultiIndex 分组的,使其更难以解释,且意义不大。

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

作者提供的图片

比较引入**.unstack()**方法后的图与上图。

(bike
 .groupby(['season', 'weathersit'])
 .agg('mean')
 .temp
 .unstack()
 .plot
 .line()
)

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

作者提供的图片

简而言之,**.unstack()**方法的作用是解开 MultiIndex DataFrame 中最内层的索引,在这个例子中是**weathersit**。这个所谓的解开索引变成了新 DataFrame 的列,这使得我们的线图绘制结果更加有意义,方便比较。

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

作者提供的图片

你还可以通过指定参数**level=0**作为**.unstack()**方法的一部分,来解开 DataFrame 中最外层的索引。让我们看看如何实现这一点。

(bike
 .groupby(['season', 'weathersit'])
 .agg('mean')
 .temp
 .unstack(level=0)
 .plot
 .line()
)

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

作者提供的图片

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

作者提供的图片

☕️ 方法 #8: .pipe()

从我的观察来看,当你在线搜索时,你几乎看不到普通人在他们的 Pandas 代码中实现这个方法。一个原因是,**.pipe()**有一种神秘的无法解释的气质,使得它对初学者和中级用户都不友好。当你去 Pandas 文档²时,你会发现的简短解释是***“应用期望 Series 或 DataFrame 的可链式函数”***。我认为这个解释有些混淆,并且对那些从未使用链式操作的人并不十分有帮助。

简而言之,**.pipe()**为您提供的是使用函数继续方法链技术的能力,在您无法找到直接解决方案执行操作返回 DataFrame 的情况下。

方法**.pipe()**接受一个函数作为参数,通过这个函数可以定义一个链外方法,然后将该方法作为**.pipe()**方法的参数引用。

使用.pipe(),您可以将 DataFrame 或 Series 作为自定义函数的第一个参数传递,并且该函数将应用于传递的对象,随后是指定的任何其他参数。

大多数情况下,您会看到**.pipe()**方法中的一行 lambda 函数,以方便使用(例如,在链处理过程中的某些修改步骤后获取最新 DataFrame)。

让我用一个简化的例子来说明。假设我们想要了解以下问题:“对于 2012 年,每个季节相对于该年总工作日的工作日比例是多少?”

(bike
 .loc[bike.index.year == 2012]
 .groupby(['season'])
 .workingday
 .agg(sum)
 .pipe(lambda x: x.div(x.sum()))
)

在这里,我们使用**.pipe()**将函数注入到我们的链式方法中。由于执行了**.agg(sum)**之后,我们无法继续使用**.div()**进行链式处理,因此以下代码将无法工作,因为我们在链式处理过程中失去了对 DataFrame 的最新状态的访问。

#Doesn't work out well!
(bike
 .loc[bike.index.year == 2012]
 .groupby(['season'])
 .workingday
 .agg(sum)
 .div(...)
)

提示:如果找不到继续链接方法的方式,请考虑如何使用.pipe()!大多数情况下,它会有所帮助!

结语

这就是低估的宝石💎第一部分的全部内容!这些都是我以前没有经常使用的方法,也许是因为我的坏习惯——用“只要能用,就够好!”的思维强行解决代码问题。不幸的是,这样做是不够好的!

只有在我花时间学习如何正确使用它们之后,它们才被证明是救命稻草,可以这么说!我还要感谢 Matt Harrison 和他的书《高效 Pandas》³,这本书彻底改变了我编写 Pandas 代码的方式。现在,我可以说我的代码更简洁、更可读,而且更有意义。

在低估的宝石第二部分中,我们将介绍其他八个较少为人知的 Pandas 方法,例如**.explode()****.melt()****.expanding()**!您可能不会经常使用它们,但在需要它们的时候它们肯定很方便。

如果你从本文中获得了一些有用的东西,请考虑在 Medium 上给我关注。每周一篇文章,轻松跟上时代潮流!

联系我!

参考资料

  1. Fanaee-T,Hadi。 (2013)。自行车共享数据集。UCI 机器学习库。doi.org/10.24432/C5W894

  2. Pandas 文档: pandas.pydata.org/docs/reference/frame.html

  3. Matt Harrison 的《Effective Pandas》: store.metasnake.com/effective-pandas-book

通用模型的非凡有效性

原文:towardsdatascience.com/the-unreasonable-effectiveness-of-general-models-b4e822eaeb27

在一个极其困难的问题上测试通用模型

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

·发表于 Towards Data Science ·8 分钟阅读·2023 年 1 月 17 日

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

[作者提供的图像,由 Excalidraw 制作]

上一篇文章 中,我尝试揭示一种有些模糊的观点,即一堆模型(每个模型专注于数据集的一个子集)应该比单一模型表现更好。

为了做到这一点,我取了一部分数据集(例如,仅美国客户),并在该组上训练了一个模型,即 专用模型。然后,我在整个数据集(即所有客户,无论其国籍)上训练了第二个模型,即 通用模型。最后,我比较了这两个模型在只包含该组观察结果的保留集上的表现。

我在多个数据集和同一数据集的多个组上重复了这个过程,总共进行了 600 次比较。 最终的赢家是通用模型

然而,我的实验并没有说服一些人,他们认为我的方法过于简化。例如,这是我在 LinkedIn 上关于该文章的帖子下最受欢迎的评论之一:

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

[从我 Linkedin 帖子] 评论区的截图

这个评论引起了我的兴趣,所以我决定按照建议去做。如果你想看看结果如何,请耐心等待。

一个工作假设

在我之前的文章中,我证明了当数据集中组成各组之间存在一定相似性时,使用通用模型相对于专用模型有明显的好处。

然而,随着组之间的差异越来越大,合理地预期使用通用模型的好处会越来越小。在最极端的情况下,即当组之间完全不同时,两种方法之间的差异应该等于零。

如果我的直觉是正确的,我们可以将这种关系草图如下:

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

我的工作假设的草图。你可以在这里阅读“上一篇文章”。[图片由作者提供,使用 Excalidraw 制作]

但这只是我的假设。那我们来尝试一下。

给我们的模型增加难度

我们的目标是回答这个问题:

如果组成一个数据集的组彼此完全不同,而我们仍然使用一个通用模型,会发生什么?

所以,问题变成了如何模拟这样的场景。

最极端的想法是“粘合”不同的数据集。当我说“不同”时,我指的是数据集不仅有不同的列,还有不同的任务,即它们旨在预测不同的内容。

以三个数据集为例:

  • “银行”:每一行代表一个银行客户,任务是预测他/她是否会订阅定期存款;

  • “员工”:每一行代表一名员工,任务是预测他/她是否会离开公司;

  • “收入”:每一行代表一个人,任务是预测他/她的收入是否超过 50k 美元。

将这些数据集的目标变量粘合在一起很简单:由于它们都是由 0 和 1 组成的二元变量,这很直接。但当我们尝试连接特征时,情况变得更加复杂。我来解释一下原因。

这里是三个数据集的一个样本(包括行和列)。

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

三个示例数据集:“银行”、“员工”和“收入”。[图片由作者提供]

如你所见,这些数据集有不同的列。那么,我们如何将它们合并在一起?第一个最简单的想法是使用pd.concat

pd.concat([X_bank, X_employee, X_income]).reset_index(drop=True)

但是,如果我们这样做,我们将得到以下形式的数据框:

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

第一次尝试:简单的连接。[图片由作者提供]

Pandas 默认只连接具有相同名称的列。在这种情况下,每个数据集具有不同的列名,因此结果具有类似对角线的结构。但这并不令人满意,因为它会让模型走捷径。事实上,模型能够根据非空列隐式地区分不同的数据集。

为了避免这种情况,我们需要一种“强制”合并不同数据集列的方法

我能想到的唯一方法是用递增的数字重命名每个数据集的列:“feature_01”,“feature_02”等。但这行不通,因为列有不同的类型。所以我们需要区分分类特征和数值特征:“cat_feature_01”,“cat_feature_02”等和“num_feature_01”,“num_feature_02”等。此外,我决定按重要性降序排列特征。

这是得到的输出:

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

第二次尝试:用递增的数字重命名列。[作者提供的图片]

也许你会认为这还不够。毕竟,模型可能仍然识别出属于某个数据集的某些类别(例如,列“cat_feature_01”中的“已婚”仅存在于“银行”数据集中)。数值特征也是如此(例如,列“num_feature_02”中的 0 到 1 之间的值仅存在于“员工”数据集中)。这对模型仍然可能有帮助,我们希望避免这种情况。

因此,作为额外步骤,我:

  • 将每个分类特征的每个值映射到不同的整数(序数编码);

  • 通过减去均值并除以标准差,对每个原始数据集的数值列进行了标准化。

所以,这就是最终结果:

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

第三次也是最后一次尝试:序数编码、标准化,然后用递增的数字重命名列。[作者提供的图片]

免责声明

我知道你可能会觉得这个过程——巧妙地将一些完全不相关的数据集合并在一起——有点奇怪。你是对的:我们所做的在现实世界中没有意义。

但你必须记住,这是一个教学实验,旨在推动通用模型的能力到极限,并观察它是否仍然能与专用模型竞争

这个实验必须被视为对基于树的梯度提升模型能力的一种“压力测试”。

结果

现在我们已经设计了策略,是时候将其应用于一些实际数据集了。我使用了 7 个用于二分类的具有 5000 多行的数据集,这些数据集可以在Pycaret(一个MIT 许可证下的 Python 库)中找到。

这些是数据集,及其相应的行数和列数:

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

Pycaret 数据集及其形状。[作者提供的图片]

然后,我应用了上述程序,这意味着我对每个数据集分别执行了以下操作:

  • 我将每个分类列(按重要性降序排列)重命名为“cat_feature_01”,“cat_feature_02”,…,每个数值列(按重要性降序排列)重命名为“num_feature_01”,“num_feature_02”,…;

  • 对每个分类列,我将每个值映射到一个不同的整数:0,1,2,…;

  • 对于每一列数值数据,我已经通过减去均值并除以标准差来标准化值;

  • 我添加了一个包含数据集名称的列。

然后,我将所有原始数据集合并以获得最终数据集。在此基础上,我进行了实验。实验内容包括:

  • 在合并后的完整数据集上训练一个通用模型(Catboost,未进行参数调优);

  • 训练了 7 个专业模型(Catboost,未进行参数调优),每个模型对应一个原始数据集;

  • 比较通用模型和专业模型在每个数据集上的表现。

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

在数据集的一组上比较通用模型和专业模型的过程。[作者提供的图片]

我在查看结果时注意到的第一件事是通用模型的预测与专业模型的预测之间的相关性为 98%,这表明它们产生了非常相似的输出。

那么性能如何呢?

这是通用模型与专业模型 ROC 分数的比较:

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

ROC 分数对比。[作者提供的图片]

通用模型的 ROC 分数与专业模型的 ROC 分数之间的平均差异是 -0.53%。这意味着专业模型通常优于通用模型

然而,我必须说,我对这种微小差异感到印象深刻。我们在一个极其困难的设置中进行了测试,通用模型仍然能够达到非常接近专业模型的性能。这证明了通用模型在这个极其困难的问题上是多么有效。

那么解释性如何呢?

我听到关于通用模型的另一个担忧是其所谓的解释性不足。事实上,有些人声称,一个通用模型的透明度不如多个专业模型。

我不同意这一点。事实上,得益于 SHAP 值,即使模型是唯一的,你也可以分别解释每个组。我们可以称这种过程为“专业化解释性”。

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

专业化解释性。[作者提供的图片]

让我用我们之前的实验做一个例子。

如果我们分别考虑每个组并计算原始特征值与相应 SHAP 值之间的相关系数,我们得到的结果是:

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

每个合并特征与相应 SHAP 值之间的相关性。[作者提供的图片]

如你所见,不同组之间的相关系数变化很大。例如,如果我们取“num_feature_01”,则“bank”组的相关性为正,而“employee”组的相关性为负。这非常有意义:

  • 对于“银行”组,“num_feature_01”对应于“duration”特征,即账户持有者的持有时间。目标特征是客户是否订阅了定期存款。可以合理地预期该特征对预测有积极影响。

  • 对于“员工”组,“num_feature_01”对应于“satisfaction_level”特征。由于目标特征是员工是否离职,因此负相关性可以很容易解释。

结论

在这篇文章中,我模拟了对一般模型而言最困难的情境:一个数据集组成的各组完全不同的案例。

为了模拟这种情况,我合并了一些完全无关的数据集,包括特征和预测任务!我使用了一个技巧,以确保不同数据集的列即使名称不同也能连接在一起。

然后,我在合并的数据集上训练了一个一般模型和许多专门模型:每个原始数据集一个。

这是一个压力测试,用来查看在对一般模型来说极端困难的情况下会发生什么。然而,我发现性能差异最小:使用一般模型代替专门模型的 ROC 评分平均损失为 0.53%。

此外,我还利用这个实验展示了为什么可解释性也不应成为问题。实际上,使用一般模型后,人们仍然可以通过“专门的可解释性”分别解释单独的组。

你可以在这个笔记本中找到本文使用的所有 Python 代码。

感谢阅读!

如果你觉得我的工作有用,可以订阅 每次我发布新文章时获取邮件 (通常每月一次)。

如果你想支持我的工作,可以 请我喝杯咖啡

如果你愿意, 在 LinkedIn 上添加我

非结构化数据漏斗

原文:towardsdatascience.com/the-unstructured-data-funnel-245f72925176

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

你向下挖掘的深度决定了你支付的费用。照片由Ricardo Gomez Angel提供,来自Unsplash

为什么漏斗成为数据最重磅竞争者之间战争的中心

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

·发表于Towards Data Science ·9 分钟阅读·2023 年 12 月 15 日

如果你不是 Medium 会员,你可以在这里免费阅读。

介绍

非结构化数据呈现出多种形式。它通常以文本为主,但也可能包含日期、数字和字典等数据。数据工程师通常会遇到以深层次的嵌套 JSON形式出现的非结构化数据。然而,“非结构化”数据这个术语实际上是指任何非表格化的数据;事实上,超过 80%的全球数据是非结构化的

尽管对我们数据从业者而言,非结构化数据似乎无害,但它在宏观层面上产生了巨大影响。实际上,GPT 模型都经过训练以处理非结构化数据。Tomasz Tunguz 在近期文章中对 Snowflake 财报电话会议的观察是正确的:

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

摘自 Tomasz Tunguz 的《Snow Angels》

在这种金融和宏观经济背景下看待非结构化数据可能显得有些奇怪。我的第一份工作是在投资银行,所以我在阅读类似内容时感到有些怀旧。“非结构化数据是增长引擎”对我来说是有意义的——这听起来像是一个非常大的市场顺风

不过,我已经有一段时间没有调整 Powerpoint 框了。从概念上讲,非结构化数据现在是一个深度嵌套的 json,等待处理。但从财报电话会议中可以看出,非结构化数据现在不仅仅是 JSON(曾经是吗?),还有文本、文档、视频等。

现在出现的是,这些数据驱动了一些即将变得至关重要的用例,而数据处理的位置对数据领域的两大巨头:Databricks 和 Snowflake 至关重要。让我们深入探讨一下原因。

为什么非结构化数据重要?

GPT模型依赖数据。具体来说,它们依赖非结构化数据。这些包括文本文档、html 文件和代码片段。随着公司越来越倾向于在生产中实现LLMs,处理这些数据的价值因其需求增加而增加。因此,它对像 Snowflake 和 Databricks 这样的供应商的价值也增加。

但是处理特定类型非结构化数据还有第二个元素。以嵌套 JSON 为例。嵌套 JSON 在处理时会被展开清理。这意味着你可能会从以下内容开始:

{
  "outer_key1": {
    "inner_key1": {
      "nested_key1": {
        "deeply_nested_key1": "value1",
        "deeply_nested_key2": "value2"
      },
      "nested_key2": {
        "deeply_nested_key3": "value3",
        "deeply_nested_key4": "value4"
      }
    },
    "inner_key2": {
      "nested_key3": {
        "deeply_nested_key5": "value5",
        "deeply_nested_key6": "value6"
      },
      "nested_key4": {
        "deeply_nested_key7": "value7",
        "deeply_nested_key8": "value8"
      }
    }
  },
  "outer_key2": {
    "inner_key3": {
      "nested_key5": {
        "deeply_nested_key9": "value9",
        "deeply_nested_key10": "value10"
      },
      "nested_key6": {
        "deeply_nested_key11": "value11",
        "deeply_nested_key12": "value12"
      }
    },
    "inner_key4": {
      "nested_key7": {
        "deeply_nested_key13": "value13",
        "deeply_nested_key14": "value14"
      },
      "nested_key8": {
        "deeply_nested_key15": "value15",
        "deeply_nested_key16": "value16"
      }
    }
  }
}

到这里:

{
  "deeply_nested_key1": "value1",
  "deeply_nested_key2": "value2"
}

处理第二个 JSON 所需的计算能力比初始清理数据时所需的计算能力要少,后者处理第一个更大的对象。这意味着数据管道中第一次“清理”发生的位置会显著影响使用的计算资源。

所有非结构化数据都遵循这种模式。Snowflake 的Document AI将像 pdf 这样的文档提取为表格形式的数据。这意味着处理的重部分只发生一次,结果数据会更加干净,处理也更容易。

非结构化数据漏斗

关注数据处理发生的位置对像 Snowflake 和 Databricks 这样的云服务提供商非常重要,因为他们基于云计算收取附加费用。这意味着你需要的计算能力越多,你支付的费用也越高。我们在前一节中看到,由于大型语言模型(LLMs),非结构化数据的重要性不断增加,但处理非结构化数据所需的计算能力随着数据在数据管道中进一步处理而减少。这是直观的,因为数据在数据管道中进展时会变得更干净、更聚合。

我们可以通过想象我们的数据管道基础设施来可视化这一点。我们中的大多数人通常拥有以下架构的子集:

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

非结构化数据漏斗。漏斗的宽度与需要处理的数据量成正比。数据在漏斗中流动时所需的计算是非线性的,因为非结构化数据清洗、数据源合并以及分析工作流等操作受到数据量以外的因素影响。想象一下作者的情况。

数据移动

漏斗的第一部分是数据团队首次接触非结构化数据的地方。这是一个数据移动层,架构上可能是批处理或流处理。这个层级没有存储元素,但像 Fivetran、Portable 或 Striim 这样的供应商正在进行一些转换(“ETL”或“ET L”而非“ELT”或“EL T”),这需要计算并减少传递到下一个层级的数据量。

这些工具在处理能力上受到限制,因为它们没有完整的数据历史记录,因此不能执行复杂的操作,如回填或慢变维度。然而,它们适用于简单的转换,如流连接或非结构化数据的去嵌套。大多数这些供应商反正不处理像文本文件这样的非结构化数据。你可以使用云原生服务来执行这些服务,例如 Azure EventHub、BigQuery PubSub,因此这些标志也涉及到漏斗的这一部分。

可用计算资源:中等

数据湖 / 对象存储

第二层指的是对象存储中的数据,如 Google Object Storage、AWS S3 或 Azure ADLS Gen-2。这些是三大云提供商的存储解决方案,可以存储任何文件格式的数据。漏斗的这一层是所有数据集中化的第一个地方,计算资源以各种形式轻松获得,可以直接从云提供商租用或通过像 Databricks 这样的供应商获取 Spark。这一层非常适合复杂的处理,尤其适合维度和复杂度的减少。这意味着涉及的计算资源异常高。

在我看来,这一层是处理非结构化数据最有意义的地方。你可以在这里存储任何东西。你拥有大量兼容的云基础设施(所有东西都可以与 S3 交互)。你有一个现成的存储层来存储你的处理数据。在这里进行这种处理非常有意义,因为它本质上比数据仓库更灵活。数据湖是为了存储任何格式的数据而构建的——这不正是非结构化数据的特性吗?

可用计算资源:非常高

数据仓库 / SQL 层

漏斗的这一层指的是以特定格式或特定格式集合存储的数据,通常这样做是为了使其能够使用类似 SQL 的语句轻松查询和处理。Snowflake 有自己的文件格式,这并不是什么秘密,它促进了这一点,也正是你支付“入口费用”的原因,以便将数据“导入 Snowflake”。在 Databricks 方面,他们有.delta,它实际上是对.parquet 的抽象,而且是开源的。还有其他如.iceberg 这样的格式,它们是“酷”的,并且促进了 Snowflake 中的外部表(这使得 Snowflake 在漏斗层级上提升了一个层次)。

这个领域适合处理结构化数据或表格数据。然而,如展平 JSON 这样的操作也可以在这里进行(例如,参见BigQuerySnowflake)。数据工程师普遍认为这不是 SQL 计算的最佳使用方式。主要反对意见是,增量执行此类操作既具有挑战性又缓慢,因此成本高昂。然而,实际成本已经在移动数据时产生。你已经为数据通过漏斗支付了费用。如果在漏斗更高的位置进行转换或解构,那么移动通过漏斗的体积就会减少,成本也会更低。

有趣的是,这个领域在 Snowflake 财报电话会议上引起了关注。考虑到我与大多数工程师交流时发现他们倾向于在漏斗尽可能高的位置进行数据处理以节省成本,"只需将数据导入 Snowflake"的口号可能正与企业数据高管和首席信息官们产生共鸣。也有可能像 DocumentAI 和 SnowPark Container Services 这样的功能实际上意味着 SQL Warehouse 与“Data Lakehouse”之间的界限正在模糊——同样的计算可以用于漏斗不同部分的数据。

需要注意的是,你可能会为在数据仓库中处理非结构化数据的计算处理支付双重加价。如果你使用像 Databricks 这样的服务,你会利用云提供商的计算能力处理存储在对象存储中的文本文件,Databricks 作为中介,因此你只需为计算支付一次加价。如果使用数据仓库,你则需要支付存储和计算的加价。而且你也不知道他们究竟是如何利用这些计算来分析文档的,因此你可能会多花一点钱。如果仓库支持外部表,你可以避免前者。

为了完成这一层——高效运行 SQL 负载需要数据处于不同的特定格式。这催生了数据仓库市场,将这些数据转换为可查询和可转换的格式,如.delta。非结构化数据在某些方面正好相反,似乎属于它已经存在的地方,即数据湖。因此,在 LLM 的背景下,使用来自仓库层的计算来处理非结构化数据并没有真正的意义。确实,一些流行的 Snowflake 用例,如 Snowpark 容器服务和外部表,已经更像是一个类似于湖屋的计算/存储模型。

计算的争夺:高

数据激活——分析的“最后一公里”

最后提到的是漏斗的结束部分——数据激活。这是分析的“最后一公里”,通常涉及进行小检查以确保流程能够启动,然后将清理过的数据移至操作系统。这指的是与清理和汇总的数据进行交互的应用程序。

这些可能是经典的反向 ETL 用例、仪表盘或自动化的 Slack/电子邮件警报。

在这一点上,你的所有数据(结构化和非结构化)可能已经得到了充分的清理。你的数据工程、分析工程和产品团队可能都已经对其进行了仔细的审查、测试、清理和汇总。

这意味着从计算方面来看,真的没有什么需要做的了。

因此,非结构化数据漏斗中最不感兴趣的部分提供了最少的机会来对计算收费给数据团队。不仅所有的数据都是干净、扁平和完美的,而且也要小得多。

计算的争夺:低

结论

本文为数据从业者提供了一种在非结构化数据背景下思考数据处理的新方式。非结构化数据的独特之处在于,文本文件、HTML 文件、深度嵌套的 JSON 以及所有介于其间的内容都需要一个初步的、繁重的计算步骤来从数据中提取价值。这一步发生在哪里对大型数据云服务提供商来说很重要,因为它与使用量以及因此产生的收入密切相关。目前特别有趣的是,LLM 和 GPT 模型的部署已经创造了对这些之前对大多数数据从业者来说相当无用和无趣的数据的需求。

通过漏斗图示,我们可以看到数据团队“延迟”这个繁琐的处理步骤时间越长,成本越高,因为移动的数据总量更大。此外,考虑到各种文件格式和操作类型的需求,似乎最合理的做法是专注于在对象存储级别处理数据。这一点通过 Snowflake 显然“向上发展”以促进非结构化数据处理得到了很好的说明。

这也引出了一个问题,即云服务提供商是否会继续向上发展。解析来自 Gmail 的电子邮件可能足够简单。假设有一个专门将数据从 Gmail 移动到对象存储的工具——为什么这个工具也不能执行“T”;在这种情况下,解析出有用的信息(大概是使用 AI),并将平坦的记录表直接转储到 S3 中?是否因为大多数有用的非结构化数据存在于 Google 和 Microsoft 的云数据库中(它们既不是 Databricks 也不是 Snowflake,且拥有其自有的数据产品套件)?

我坦率地说,不知道这个问题的答案是什么。不过,我确定处理尽可能多的数据会有巨大的经济激励。处理到什么程度是一个有待讨论的问题,正如 Snowflake 成功说服公司在进行任何转换之前将数据迁移到 Snowflake 所展示的那样。BI 工具收费数十万美元的时代已经过去了。争夺 S3 的战斗已经开始🔥

RAG 的未被揭示的一面:解决领域特定搜索中的挑战

原文:towardsdatascience.com/the-untold-side-of-rag-addressing-its-challenges-in-domain-specific-searches-808956e3ecc8

通过混合搜索、层级排名和讲师嵌入,解决我们 RAG 设置中类似的领域特定文档问题。

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

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

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

生成式 AI 增强的搜索技术(图像由 MidJourney 生成)

Carsales 作为领先的汽车平台,服务于澳大利亚、智利、韩国和美国的汽车和生活方式车辆市场。我们的目标是重新定义汽车买卖体验,树立无与伦比的标准。为此,我们的一个关键功能是一个全面的搜索工具,能够扫描数万篇与汽车相关的编辑文章。我们目前集成了 Google 搜索——专门为我们的编辑内容量身定制并通过 iframe 展示——结果虽然不错,但主要依赖于词汇(关键词)关联,有时忽略了搜索查询的真实本质或语义。

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

使用现有的 Google 搜索结果

例如,搜索“Toyota Corolla 2020 年气囊数量?”会返回包含“Toyota Corolla 和气囊”字样的任何文章等。然而,这些文章大多讨论的是气囊召回,而非实际的气囊数量。商业方面有强烈需求,不仅要从技术上提升这个工具,还要重新设计其界面,使其更加无缝地集成到我们的网站中,超越仅仅是一个 Google 搜索结果的 iframe。

在 2023 年 7 月,我们举办了我们的全球黑客马拉松之一。这项活动通常会吸引来自各部门的参与者,组建团队,提出创意,并在紧张的三天内将这些创意转化为可用的原型。随着大型语言模型(LLMs)的日益重要,我们确定了一个理想的项目:使用 LLM 改进我们的搜索工具。更棒的是,我们赢得了这次黑客马拉松!感谢我们了不起的团队,他们为此付出了极大的努力!

在本文中,我们将首先概述我们项目的基本概念。之后,我们将谈论我们的初步 RAG 方法。随后,我们将重点讨论在确保准确和相关的文档检索及生成直接答案中面临的挑战。然后,我们将讨论我们实施的解决方案来应对文档检索难题。不过,我们将把直接答案生成的讨论留到下一篇文章中,以保持本文的简洁。

概念

我们设想的模型是简单明了的。我们希望在收到搜索查询后得到两方面的结果:

  • 相关文档:虽然这与我们现有的功能相似,但我们的目标是通过检索与查询意图相符的文档(基于意义的匹配),而不仅仅是基于词汇的匹配(直接词匹配),来提高其效能。

  • 直接回答:每当查询以问题的形式提出并且我们的系统可以从编辑资源中得出答案时,我们希望呈现这一直接答案。这个新颖的功能有望显著丰富用户互动。

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

使用我们概念化的 RAG 方法的搜索结果

在利用大型语言模型(LLMs)进行文档检索并通过组织的知识库提供直接答案时,通常有两种方法:

  • 微调 LLM:这涉及到使用您组织独特的知识库(在我们的情况下是我们的编辑文章集合)来精炼基础 LLM。这确保了 LLM 可以准确回答与这些文章相关的问题。

  • 检索增强生成(RAG):在这里,策略是将上下文相关的编辑文章与提出的问题一同提供给 LLM,使其能够生成准确的回答。

微调直接将知识融入模型中,减少了对 LLM 的大量提示的依赖。然而,这也带来了挑战:需要不断更新 LLM 以包含新的或修订的文章,并且运行微调模型的操作成本显著增加——可能是基础 LLM 的十倍。鉴于这些,大多数人,包括我们,都更倾向于 RAG 方法。

检索增强生成

检索增强生成(RAG)可能听起来很复杂,但其原理很简单。想象一下将我们的查询分成三个部分:首先,问题本身,例如“丰田卡罗拉混合动力 2020 的燃油消耗是多少?”其次,给 LLM 的指令,使用我们接下来提供的文档来构建回答。最后,是可能找到答案的文档上下文。

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

RAG 简述

从本质上讲,这就是 RAG 的核心。自然地,你可能会想知道我们如何从我们库中的成千上万篇文章中筛选出相关的文章作为文档上下文来回答提出的问题。

为什么不把所有的文章都包含在文档上下文中呢?考虑到 45,000 篇文章,每篇文章有 10,000 字,总计 4.5 亿字。最新的 LLM Claude 2 仅支持 100,000 令牌的窗口。即使未来的模型扩展了容量,相关的成本和处理时间也将是巨大的。我们的目标是根据与查询的相关性对文章进行排名。文章内容与查询越相似,它越可能包含答案。因此,我们选择最相关的 X 篇文章作为我们的文档上下文。这就是为什么我们需要向量数据库的原因。

向量数据库和密集嵌入

向量数据库本质上存储的是向量,这些向量是浮点数的数组。那么这如何解决我们的挑战呢?进入密集嵌入,这是一种 NLP 技术,它将文本(我们的文档)转换为向量形式。这种向量通常更有效地 encapsulates 文本的语义本质,从而允许更准确地比较两段文本之间的相似性。

在密集嵌入出现之前,我们通过计算文档之间的常见词汇来衡量文档的相似性。让我们用一个例子来说明。给定问题“丰田卡罗拉 2020 有多少个气囊?”,以下哪一句话与其更为贴近?

丰田卡罗拉 2020 由于 气囊 线圈以及 其他 诸多问题而被召回。

丰田卡罗拉 2020 配备了七个 气囊 和一个备份摄像头。

使用简单的词匹配,第一个句子有六个常见术语,看起来更像是问题。然而,本质上,第二个句子虽然只有四个匹配的术语,但更相关,因为它涉及到关于气囊数量的查询。即使排除停用词(have 和 many),它们仍然都有四个匹配的术语。

当我们使用密集嵌入技术将问题和两个句子转换为向量并将它们可视化到二维平面上时,第二个句子与问题的接近度变得显而易见,特别是在基于余弦距离(向量之间的角度)评估相似性时。

利用这一能力,我们可以将所有文档与给定问题相关联,只选择最相似的四篇文章作为 LLM 的上下文,而不是筛选整个 45,000 篇文档。向量大小可以根据使用的密集嵌入方法而有所不同。最初,我们使用了 OpenAI 的 text-embedding-ada-002,这种方法生成了 1536 的向量大小。

这是该过程的一个详细说明:

起初,在索引阶段,我们使用密集嵌入方法将所有文章转换为向量表示,并将这些向量保存在像 Pinecone 这样的向量数据库中。当需要推断时,我们也将提出的问题转换为查询向量,使用相同的嵌入技术。这个查询向量随后被输入到 Pinecone 中,Pinecone 检索出四篇最相关的文章。Pinecone 无缝地计算查询向量与我们存储的所有文档向量之间的余弦距离,以返回最相关的四篇文档。接下来,我们将这些选定文章的内容作为文档上下文提供给 LLM,连同原始问题和一个生成回答的指示。

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

RAG 架构

本质上,这捕捉了增强检索生成(RAG)的精髓。大多数 RAG 实现包括某些改进,以提高所选文档的相关性,从而影响最终答案的准确性,接下来我会讨论这一点。

切分我们的文档

典型的编辑文章可能长达 10,000 字。发送四篇最相关的文档作为上下文,可能意味着处理多达 40,000 字的内容,这可能是一个资源密集型的任务,特别是在频繁调用的情况下。例如,丰田卡罗拉 2020 年的编辑评论可能涉及从安全特性和定价到燃油效率的各个方面,这些信息分布在不同的段落中。往往,特定问题的答案可以仅定位于这几段内容。这就避免了传输整个文章进行处理的必要。

实际意义是什么?与其将整个文档转换为向量,不如将其分割成较小的块(通常为一到两个段落)并将这些块转换为密集向量——这个过程被称为“文档切分”。

有趣的是,文档切分不仅能节省我们的处理时间,还能提高文档检索的准确性。你会问怎么做?

以丰田卡罗拉混合动力轿车评论的一段为例:

新款丰田卡罗拉满足了很多要求。值得一提的是,这款四门车型比其五门掀背车长 255 毫米,车长为 4630 毫米。

车内配备了许多有用的功能,如全系标配的 Apple CarPlay 和 Android Auto,并显示在一块 8 英寸的触摸屏上。所有车型还标配自适应雷达巡航控制。

尽管动力不足,这款混合动力 Corolla 轿车却非常省油。声称的燃油经济性为 3.5L/100,这使它成为该国第二高效的丰田车辆。

如观察所示,第一个段落讨论了汽车的尺寸,第二个段落详细说明了数字设备的功能,而第三个段落则专注于燃油效率。

如果我们将这些段落分别分成不同的密集向量,我们将生成三个独特的文档向量,这些向量位于不同的嵌入空间,每个向量象征着不同的主题。

假设查询是“Toyota Corolla 的尺寸是多少?”。第一个段落变成最相关的文档。对于类似“CarPlay 是否可用?”的问题,第二个段落优先考虑。

然而,将所有三个段落合并成一个密集向量会产生一个更为泛化的向量,这可能无法有效地指向特定的主题。嵌入文档越大,向量越被稀释,最终无法准确捕捉特定主题。随着时间的推移,由于密集向量包含了许多重叠的主题,如安全性或内部特征,其独特性减少,从而降低了文档检索的精度。

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

随着文档块变大,密集嵌入失去了其特异性

鉴于这些因素,我们将文档划分为块,每块包含 300 个标记。此外,我们还使用了 Langchain 框架中的 RecursiveCharacterTextSplitter 技术。

这种方法对文本进行层次化分类:首先优先考虑段落,然后是句子,最后是单个单词。我们的目标是尽可能在 300 个标记的范围内保留整个段落。如果不能,则将其分解为句子,然后是单词。将段落分成不同的块可能会导致丢失与当前主题相关的重要背景。

为了增强每个段落内的上下文连续性,我们确保后续段落与前面的段落重叠 100 个标记。这种策略确保了当一个长段落被分成多个段落时,这些段落之间的共享词语有助于减少潜在的上下文丢失。

例如,考虑下面的段落。

在其他地方,备受期待的 Model S 更新借鉴了较小的 Tesla Model 3 和即将推出的 Tesla Model Y 的内部元素,包括更新的 17.0 英寸中央触摸屏显示器,具备卫星导航、互联网功能,以及大量游戏、视频流媒体和特斯拉备受推崇的“复活节彩蛋”。

当段落被分成两个独立的部分而没有任何重叠内容时,后续部分将失去对讨论的具体车型的参考。

块 1:

另外,期待已久的 Model S 改款借鉴了较小的特斯拉 Model 3和即将推出的特斯拉 Model Y的内饰元素,包括更新的 17.0 英寸中控触摸屏

块 2:

中控触摸屏具备卫星导航、互联网功能,并且有大量游戏和视频流服务。

由于内容重叠(用粗体标出),上下文现在得到更好的保留。

块 1:

另外,期待已久的 Model S 改款借鉴了较小的特斯拉 Model 3和即将推出的特斯拉 Model Y的内饰元素,包括更新的 17.0 英寸中控触摸屏,具备卫星导航、互联网功能,并且有大量游戏和视频流服务。

块 2:

特斯拉 Model 3和即将推出的特斯拉 Model Y的内饰元素,包括更新的 17.0 英寸中控触摸屏,具备卫星导航、互联网功能,并且有大量游戏和视频流服务。

注入高级上下文

在我们的编辑文章中,段落通常省略了对具体车型的明确提及。这通常是因为从文章标题或早期段落中可以隐含得出。以以下第二段为例。虽然读者从之前的上下文中知道这是关于丰田卡罗拉 2020 混合动力轿车的,但该特定型号在段落中并未明确提及。设想有 20 篇不同的文章段落,每篇文章都讨论了不同的汽车但都提到了像Apple CarPlayAndroid Auto这样的共同特性。当每个段落变成单独的文档块时,它们的密集向量嵌入可能会非常相似。因此,如果用户问:“丰田卡罗拉 2020 混合动力轿车是否配备了Apple CarPlayAndroid Auto?”,查询向量可能与这 20 个文档块中的任何一个相似,导致检索困难。

新款丰田卡罗拉满足了很多要求。值得一提的是,这款四门轿车比它的五门掀背车长了 255 毫米,总长度为 4630 毫米。

内部配备了许多实用功能,如覆盖整个系列的Apple CarPlayAndroid Auto,并显示在一个 8 英寸的触摸屏显示器上。自适应雷达巡航控制也是所有车型的标准配置。

虽然动力不是很强劲,但混合动力卡罗拉轿车非常省油。宣称的燃油经济性为 3.5L/100 公里,使其成为在这个国家中第二高效的丰田车型。

为了应对这一挑战,我们在每个文档块的开头加入文章标题,通常会说明汽车的品牌、型号和发布年份。这一整合确保了密集向量嵌入反映这一关键信息。

标题:丰田卡罗拉 2020 混合动力轿车评测

新款丰田卡罗拉满足了很多要求。值得一提的是,这款四门轿车比它的五门掀背车长了 255 毫米,总长度为 4630 毫米。

标题:丰田卡罗拉 2020 混合动力轿车评测

内部配备了许多实用功能,如覆盖整个系列的Apple CarPlayAndroid Auto,并显示在一个 8 英寸的触摸屏显示器上。自适应雷达巡航控制也是所有车型的标准配置。

标题:2020 年丰田卡罗拉混合动力轿车评测

尽管起步加速不算迅猛,但混合动力卡罗拉轿车却极其省油。宣称的燃油经济性为 3.5L/100 公里,使其成为在本国可用的第二节能的丰田车辆。

采用单样本学习

大型语言模型(LLMs)在没有直接培训的任务上表现优异。将指令与示例配对通常会产生更好的结果。执行没有示例的任务称为零样本学习,而有示例的任务称为单样本学习。遵循常见的 RAG 方法论,我们使用单样本学习,提供一个示例。

引入源文章引用

我们的下一个目标是显示答案的来源。引用来源通过提供可追溯的参考来提升用户体验,促进更深入的探索和验证。这种透明度增强了用户信任,使他们能够验证答案,从而提高我们平台的信心。此外,这可以减少大型语言模型如 LLMs 中常见的“幻觉”问题,这些模型有时会提供错误的答案。通过让 LLMs 引用来源文章,我们避免了错误回答,因为它们不能引用不存在的来源。这类似于人类行为:人们在没有可验证来源的情况下会犹豫回答。以下是我们的用户提示,基于Langchain模板,指导 LLM 在 SOURCES 字段中包含来源。请确保你在回答中始终返回一个“SOURCES”部分。

我们在{question}占位符中注入了实际问题,并在{document-context}占位符中注入了我们的四个主要文档块及其来源。

Given the following extracted parts of a long document (with its source) and a question, create a final answer with references ("SOURCES"). 
If you don't know the answer, just say that you don't know. Don't try to make up an answer.
ALWAYS return a "SOURCES" part in your answer.QUESTION: Which state/country's law governs the interpretation of the contract?=========
Content: This Agreement is governed by English law and the parties submit to the exclusive jurisdiction of the English courts in  relation to any dispute (contractual or non-contractual)...
Source: 28-plContent: No Waiver. Failure or delay in exercising any right or remedy under this Agreement shall not constitute a waiver of such (or any other)  right or remedy.\n\n11.7 Severability.
Source: 30-plContent: (b) if Google believes, in good faith, that the Distributor has violated or caused Google to violate any Anti-Bribery Laws (as  defined in Clause 8.5) or that such a violation is reasonably likely to occur,
Source: 4-pl=========
FINAL ANSWER: This Agreement is governed by English law.
SOURCES: 28-plQUESTION: What did the president say about Michael Jackson?
=========
Content: Madam Speaker, Madam Vice President, our First Lady and Second Gentleman. Members of Congress and the Cabinet. Justices of the Supreme Court. My fellow Americans.
Source: 0-plContent: We have lost so much to COVID-19\. Time with one another. And worst of all, so much loss of life.
Let's use this moment to reset. Let's stop looking at COVID-19 as a partisan dividing line and see it for what it is: A God-awful disease.
Source: 24-plContent: And a proud Ukrainian people, who have known 30 years  of independence, have repeatedly shown that they will not tolerate anyone who tries to take their country backwards.
To all Americans, I will be honest with you, as I've always promised. A Russian dictator, invading a foreign country, has costs around the world. And I'm taking robust action to make sure the pain of our sanctions  is targeted at Russia's economy.
Source: 5-plContent: More support for patients and families. To get there, I call on Congress to fund ARPA-H, the Advanced Research Projects Agency for Health. It's based on DARPA-the Defense Department project that led to the Internet, GPS, and so much more.
Source: 34-pl=========
FINAL ANSWER: The president did not mention Michael Jackson.SOURCES:
QUESTION: {question}=========
{document-context}
=========FINAL ANSWER:

在实施我们的系统后,我们迫不及待地将 RAG 投入测试,期待与 LLM 的首次互动。然而,一旦初期的热情消退,我们发现了一些问题,主要涉及两个方面:文档检索的质量和生成的直接回答的准确性。这在我们几乎一半的测试案例中发生了。

说实话,直接回答相关的大部分问题源于所选文档作为上下文的质量不够充分。然而,本文将专注于文档检索挑战及我们解决这些挑战的策略。敬请关注即将发布的文章,深入探讨答案生成的复杂性。

问题

与年份不相关的汽车文章排名较高

当我们用查询“2018 年马自达 CX-9 评测”搜索 Pinecone 数据库时,系统返回了三个文档片段。令人惊讶的是,前两个结果来自马自达 CX-9 2019 年和 2017 年评测文章,而 2018 年评测(我们实际的目标)仅排名第三。

(1) 标题:2019 年马自达 CX-9:视频评测

马自达 CX-9 已经成为 Carsales 办公室的最爱。它获得了我们 2016 年年度汽车奖,并且在大多数多车比较中仍能排在前列。2019 年,CX-9 进行了更新,马自达表示,它已经解决了之前存在的一些小问题…

(2) 标题: 马自达 CX-9 2017 年评测

CX-9 型号,仅次于 Azami,GT 版本无论内外都相当豪华。二十英寸的轮毂、镀铬装饰、无钥匙进入和 LED 行车灯赋予我们的“魂红”版外观高贵…

(3) 标题: 马自达 CX-9 2018 年评测 — 澳大利亚

马自达 CX-9 系列中有一款新车型。顶级的 CX-9 Azami LE 本月将在澳大利亚上市,配备了更多的设备、技术,并且重要的是,增加了奢华感。它成为了 2019 年款马自达 CX-9 系列的亮点…

对于查询“马自达 CX-9 2019 年油耗是多少?”,系统未能提供关于马自达 CX-9 2019 年款油耗的具体结果。所有返回的文档都是不同年份的。第一个是 2010 年款的文档。讨论马自达 CX-9 2019 年油耗的文档标题为“马自达 CX-8 与马自达 CX-9 2019 年比较”。这个文档竟然排名第六,这意味着它没有作为文档上下文提供给 LLM 生成直接答案。

(1) 标题: 马自达 CX-9 现在更瘦、更轻、更亮

马自达正逐年降低 CX-9 型号的综合循环油耗。三年前发布时,CX-9 的油耗为 13.0L/100km。去年的一次升级将这一数据减少到 12.2L/100km,而昨天宣布的另一项升级正式将油耗再次降低至 11.3L/100km…

(2) 标题: 马自达 CX-9 2016 年评测

170kW、2.5 升四缸涡轮增压汽油发动机,配合六速自动变速器。没有手动选项或柴油动力系统,这也得归功于该车重心放在开发美国市场。尽管如此,这款汽油发动机仍然能够在其尺寸的车辆中实现令人印象深刻的燃油经济性,承诺综合循环油耗为 8.8L/100km…

(3) 标题: 马自达 CX-9 2018 年评测

油耗也是一个持续考虑的因素,我们已经行驶了数千公里,大多数情况下车上有两个人,油耗读数仍然徘徊在 10.0L/100km 左右。注意:这一最新的油耗数据比 CX-9 的两驱版本略高,后者的平均油耗为 9.2L/100km,这一数据来源于行程计算机和我们自己的独立测试。两者都高于其官方数据…

(6) 标题: 马自达 CX-8 与马自达 CX-9 2019 年比较

马自达 CX-9 是一款评价非常高的车辆,曾获 carsales.com.au 年度汽车奖以及其他多个奖项。但它仅配备了 170kW/420Nm 的 2.5 升四缸涡轮增压汽油发动机,搭配六速自动变速器。再加上全轮驱动——正如我们在这次测试中自然地做的——整车重量达到 1924 公斤。但马自达仍然声称其合理的油耗为 8.8L/100km。我们将在高速公路、小道和泥土路上进行测试…

文档检索未返回优先考虑文章时效性的文章

进一步检查显示,我们的文档检索系统忽略了文章的时效性,常常忽视优先考虑更新内容。

例如,搜索“丰田卡罗拉”会得到 2010 年和 2013 年的文章作为前几个结果。令人惊讶的是,关于丰田卡罗拉 2020 年的更相关文章,由于其时效性,应该更受关注,却被排在了第 15 位。

(1) 标题:丰田公布下一代卡罗拉轿车

额外的轮胎隔音。美国市场的标准配置包括八个安全气囊、蓝牙、空调、玻璃内置收音机天线、60/40 分割后座靠背以及 LED 近光灯和日间行车灯——丰田称这是小型车市场中的首创…

(2) 标题:丰田卡罗拉:2013 年小型车大测试

丰田卡罗拉 Ascent Sport(掀背车)我们喜欢的:材料质量有所提升 性能敏锐 价值令人印象深刻 不喜欢的:平坦的仪表板设计 繁琐的触摸屏 没有后座通风口 路上表现 当丰田章男在 2009 年掌控全球最大的汽车公司时,他设定了一个目标,即注入…

(15) 标题:丰田卡罗拉轿车 2020 年评测

丰田卡罗拉是全球销量最高的汽车之一——但大多数人想到(并购买)的是掀背车。卡罗拉轿车一直有些,嗯,被遗忘。但现在风向有所改变。全新的设计、更高的配置水平、更具吸引力的驾驶体验…

特定汽车品牌型号的文章排名高于通用文章

经过进一步分析,可以明显看出,像“什么是混合动力车?”这样的查询,结果很混杂。返回的第一篇文档是通用的,第二篇则介于两者之间,而第三篇和第四篇专门讨论本田混合动力车。对于此类查询,大多数人更倾向于阅读广泛讨论混合动力车概念、优点和缺点的内容,而不是特定汽车品牌或型号的具体信息。显然,我们的系统未能完全满足这些期望。

(1) 标题:什么是混合动力车?

在动物世界中,混合体是指两种不同物种或品种之间的杂交。在汽车世界中,这个术语已经演变为定义一种既由电动机驱动又由内燃机驱动的汽车。电动机承担了大部分工作,但在混合动力车的最初概念中…

(2) 标题:混合动力车即将蓬勃发展

它是一款混合动力车。然而,它不是普通的混合动力车,如普锐斯或思域。这是一款配备汽油 V6 发动机和传统后驱的汽车,具有现代外观和高科技、符合人体工程学的设计。丰田产品开发和工程经理 Doug Soden 预计,澳大利亚的混合动力车销量可能会 …

(3) 标题:本田混合动力车是一款不错的运动型车

2007 年 2 月 23 日。混合动力技术在即将召开的日内瓦车展上,霍多纳展示了一款体育车原型(如图),将混合动力技术与性能爱好者相结合。小型混合动力运动概念车使用了先进的混合动力技术 …

(4) 标题:本田思域(2001-)

未来 50–100 年。燃料电池是当前的未来趋势,但可负担的商业化仍然至少需要 20 年,因此在此期间,许多汽车制造商正走向混合动力路线。混合动力车本质上是由小型汽油发动机和额外的电动机驱动的传统汽车,电动机由可充电电池供电 …

关于错误模型的文章正在被返回

在搜索“Mazda CX-9 2023 评测”时,系统主要检索到了关于 Mazda CX-8 的文章,这是一个错误的模型,尽管这些文章来自 2023 年。一篇关于 Mazda CX-9 的 2021 年文章被藏在结果的更远处,排名第五。一般来说,用户期望看到与他们搜索的特定品牌和型号相关的文章,即使是来自前一年,而不是关于查询年份的完全不同模型的内容。

(1) 标题:Mazda CX-8 2023 评测

自 2018 年上市以来,Mazda CX-8 三排家庭 SUV 一直是 CX 系列中更受欢迎车型的配角。但随着 CX-9 在年底前停产以及一系列全新、更昂贵的 SUV 即将首发,是时候让 CX-8 登上聚光灯,显然成为了马自达最实惠的家庭车 …

(2) 标题:Mazda CX-8 2023 视频评测

Mazda CX-8 在 Mazda 全面的 CX 系列 SUV 中销售成绩一直处于末尾。但在高端车型正在进行全新一代换代的同时,获奖的 CX-9 也被停产,CX-8 现在显然成为了品牌的三排七座价格引领者 …

(5) 标题:Mazda CX-9 2021 评测

悬挂系统在调校上充分考虑了乘客的舒适度(对有孩子在车上睡觉时很好)。在隔离测试中,CX-9 在乘坐舒适性方面难以挑剔,但正如我们最近与起亚索伦托的比较所证明的那样,马自达在这一领域已经不再是领先者 …

总结一下我们文档检索系统的问题:

  • 当查询未指定车型年份时,较新的文章不会被优先考虑。

  • 系统经常根据查询中提到的年份匹配文章,即使这会牺牲对车型的匹配度。

  • 对于广泛的查询,系统有时会优先考虑特定品牌型号的文章,而非一般文章。

  • 系统无法对不同车型年份查询的相关文章进行准确排名。

认识到这些问题至关重要,这表明我们离终点还远。

经过一个集中的分析阶段,跨越了周末和深夜,我们已经锁定了主要问题。

我们的库中充满了关于相同品牌和型号的编辑文章,但来自不同年份,例如 2018 年、2019 年和 2021 年的马自达 CX-9 评测。这些文章的结构布局相似。某些部分讨论燃油效率,而其他部分则侧重于安全性或内部空间。当查询“马自达 CX-9 2019 年燃油效率?”时,会出现多个文档块,每个块都来自不同年份的马自达 CX-9 文章,涉及燃油效率。值得注意的是,每个文档块都标注了其原始文章的标题和年份。因此,可以预期“马自达 CX-8 v 马自达 CX-9 2019 比较”的密集向量与查询的密集向量相比,更加相似于“马自达 CX-9 2016 年评测”的密集向量。

然而,现实并非如此。年份仅仅是大约 300 个内容 token 中的一个单词,并没有发挥应有的作用。在评估更广泛的内容时,第一篇文章似乎比第二篇更侧重于燃油效率。密集嵌入过程未能识别年份在汽车领域中的关键作用。例如,不同年份对应着车辆的不同型号,因此在密集嵌入向量创建过程中,年份应当具有更大的权重。这一困境被标记为领域超出上下文挑战。利用基于通用数据集设计的基础嵌入模型,意味着它忽视了汽车领域的具体细节。

(1) 标题:马自达 CX-9 2016 年评测

170kW,2.5 升四缸涡轮增压汽油发动机配合六速自动变速器。没有手动选项或柴油动力系统,感谢开发此车型的重心在于美国市场,因此缺乏燃料选择。尽管如此,汽油发动机仍然能够为其大小的车辆提供令人印象深刻的燃油经济性,承诺的综合循环燃油消耗为 8.8L/100km …

(2) 标题:马自达 CX-8 v 马自达 CX-9 2019 比较

马自达 CX-9 是一款评价非常高的车辆,曾获得 carsales.com.au 年度汽车奖和其他多个奖项。但它仅配备了 170kW/420Nm 的 2.5 升四缸涡轮增压汽油发动机,并与六速自动变速器配对。加上全轮驱动——正如我们在测试中所做的——你会发现其整备质量达到 1924 公斤。但马自达仍声称其油耗为 8.8L/100km。我们将在高速公路、小路和泥土路上进行测试……

正是这个问题导致“2023 年马自达 CX-8 评测”在查询“2023 年马自达 CX-9 评测”时排名高于“2021 年马自达 CX-9 评测”。我们的标准模型未能将车型匹配优先于年份匹配,从而导致了这种差异。

一个更简单的类比是考虑“炸鸡”更类似于“鸡汤”还是“炒饭”。答案取决于上下文。如果关注的是配料,“炸鸡”与“鸡汤”更为相似。但从准备的角度来看,它更接近于“炒饭”。这样的解释是领域中心的。

许多 RAG 设置可能会绕过我们面临的挑战,因为他们的索引文章与我们的并不相似。例如,对于客户支持用例,大多数文章有不同的主题,比如“如何重置我的密码”或“如何更改我的电子邮件地址”。

解决这个问题可能需要定制我们的嵌入模型,使用以汽车为重点的数据集。然而,微调存在挑战。我们需要正样本(两个相似的句子)和负样本(两个不同的句子)数据集,这是一项巨大的工作。考虑到我们使用的是 OpenAI 的 text-embedding-ada-002 模型,微调甚至不是一个选项,因为他们尚未提供这一服务。

解决方案

我们的探索使我们找到了一种更有前景的解决方案,融合了四种策略:

  • 混合搜索(结合稠密和稀疏嵌入)

  • 层次文档排名

  • 教学稠密嵌入

  • 年份提升。

混合搜索(结合稠密和稀疏嵌入)

我们解决方案的关键在于稠密和稀疏嵌入的融合,即“混合搜索”。稀疏嵌入本质上类似于传统的关键词或词汇匹配。令人惊讶的是,这正是我们最初希望用稠密嵌入取代的传统方法。

让我们进一步讨论一下这种稀疏嵌入。

在密集嵌入的领域,每个文档块都被封装在一个 1536 维浮点向量中。然而,稀疏嵌入则将这些文档块呈现在一个扩展的维度空间中, resulting in a considerably expansive vector. 从本质上讲,基本的稀疏嵌入是一个向量,其中每个槽位代表词汇表中的一个独特词汇。如果一个词在文档块中出现,则向量中的对应槽位标记为 1;如果没有出现,则保持为 0。为了优化这一点,通常会排除“is”,“and”,“what”等停用词,以简化词汇表的大小。在我们的应用中,我们处理的词汇表大约包含 50,000 个词,相当于一个同样大小的向量。

此外,它们主要的零填充特性优化了存储和计算,因为大多数条目(为零)可以在计算过程中有效地压缩或跳过。计算两个稀疏嵌入向量之间相似度的方式与两个密集嵌入向量相似,利用的是余弦距离。

超越这种基本方法,我们遇到了像 BM25 排名 算法这样的复杂技术。虽然 BM25 的基本思想建立在广泛使用的 TF-IDF(词频-逆文档频率)方法之上——该方法根据术语在文档中的频率相对于所有文档中的频率来强调术语的重要性——BM25 对其进行了改进。BM25 不只是简单地分配二进制值或原始词频,而是为文档块中的每个术语确定一个权重。这个权重受到术语频率和逆文档频率的影响,量化了术语的相关性,使 BM25 在捕捉术语在上下文中的重要性方面更加细致。

稀疏嵌入的吸引力在于它们的准确性;它们在基于文档和查询之间的精确词对词匹配进行文档排名时表现出色。例如,使用稀疏嵌入时,查询“Mazda CX-9 2019 的燃油效率是多少?”相比于“Mazda CX-9 2016 评测”,会为“Mazda CX8 v Mazda CX-9 2019 比较”排名更高。这是因为前者有三个完全匹配的独特词汇,而后者只有两个。

然而,我们不能完全用稀疏嵌入来取代我们的密集嵌入。这样做会使我们回到最初面临的搜索系统挑战,主要是忽视语义意义。虽然密集嵌入在根据语义意义评估文档块方面表现出色,但在通过精确词匹配进行排名时却不尽如人意。相反,稀疏嵌入优先考虑精确的词匹配,但可能忽略更广泛的语义背景。因此,为了利用两种方法的优势,结合它们是至关重要的。这种融合确保了一个更全面和有效的搜索系统。

我们根据两个不同分数的加权平均值来确定文档的排名。初始分数使用与密集嵌入的余弦距离,这是一种我们之前采用的方法。同时,次级分数使用与 BM25 稀疏嵌入的余弦距离。

我们使用以下公式计算最终分数,用于文档排名:

hybrid_score = 0.75 * dense_score + 0.25 * sparse_score

sparse_score 倾向于选择那些与查询匹配的词语出现频率较高的文档。幸运的是,Pinecone 本地支持这种混合搜索机制。我们的责任是输入密集向量和稀疏向量。通过分配权重,我们强调密集/语义匹配,而不是稀疏/关键词匹配,如 0.75 对 0.25 的比例所示,这样我们可以检索到前 x 个相关的文档片段。

分层文档排名

我们的次级方法涉及一个两层的排名系统。最初,分数是基于文档片段中的内容,使用我们的混合搜索公式进行计算。随后,这些分数会得到提升,提升的分数是根据文档标题使用相同公式确定的。

理由是什么?标题虽然简洁,但通常包含了关键的信息,例如汽车的品牌、型号和生产年份,基本上是内容的浓缩总结。因此,仅根据标题对文档进行排名,比仅根据内容排名更有效,这种情况大约发生一半的时间,尤其是在稀疏嵌入的情况下。考虑查询,“Mazda CX-9 2019 的燃油效率是多少?”一篇标题为“Mazda CX-9 2019 评测”的文章将会胜过“Mazda CX-9 2018 评测”,因为前者有更多的匹配术语。

为了整合这一点,我们的公式进行了修改:

final_score = 0.5 * title_hybrid_score + content_hybrid_score

多亏了这些改进,我们已经观察到文档检索结果相关性的显著提升。例如,查询“Mazda CX-9 2019 的燃油效率是多少?”现在最终返回了来自文章“Mazda CX-8 与 Mazda CX-9 2019 比较”的正确文档片段,这归功于稀疏嵌入。

(1) 标题:Mazda CX-8 与 Mazda CX-9 2019 比较

Mazda CX-9 是一款评价非常高的车辆,曾获得 carsales.com.au 年度汽车奖以及其他多个奖项。但它只配备了 170kW/420Nm 的 2.5 升四缸涡轮增压汽油发动机,搭配六速自动变速器。再加上全轮驱动——我们在这次测试中自然也用了——你会看到一个重达 1924 公斤的车重。但 Mazda 仍然声称其合理的 8.8L/100km 的燃油消耗数据。我们将在高速公路、小路和泥泞道路上对其进行测试……

但是,仍然存在一个挑战:我们的文档检索系统并没有优先考虑基于汽车品牌/型号的匹配,而是基于型号年份的匹配。

Instructor Large Dense Embedding

问题源于我们的嵌入模型缺乏汽车行业的特定领域知识,这对于理解基于品牌/型号的匹配比基于生产年份的匹配更重要。

我们的解决方案?过渡到Instructor Large Embedding模型。这个最先进的模型提供了一个独特的优势:可以通过简单的文本提示轻松定制到特定领域或任务。除了定制能力外,该模型本身在嵌入质量上也更为优越。

仅通过向 Instructor Large 模型提供提示“为检索相关文档生成汽车评论文章问题”,它就能够巧妙地优化我们在汽车评论领域的嵌入。这一过程类似于为特定领域微调嵌入模型,但避免了相关成本。因此,我们的嵌入现在能巧妙地将汽车品牌和型号的属性置于生产年份之上。

以下代码展示了 Instructor Large 嵌入在我们特定需求中的优越性,相较于 OpenAI 的 text-embedding-ada-002。

from embedding.azure_embedding import AzureEmbedding
import numpy as npsentences = ["Mazda CX-9 2018 Review", "Mazda CX-8 2018 Review", "Mazda CX-9 2017 Review"]print("Azure Embedding")
emb = AzureEmbedding()
emb_1 = emb.embedd(sentences)
emb_2 = emb.embedd(sentences)
emb_1a = np.array(emb_1)
emb_2a = np.array(emb_2)
print(emb_1a @ emb_2a.T)
print("\n")print("Instructor-large")
from InstructorEmbedding import INSTRUCTOR
model = INSTRUCTOR('hkunlp/instructor-large')
instruction = "Represent a car review article:"
doc = [[instruction, sentence] for sentence in sentences]embeddings_1 = model.encode(doc)
embeddings_2 = model.encode(doc)
similarity = embeddings_1 @ embeddings_2.T
print(similarity)
print("\n")

我们展示了两种不同的嵌入方法,任务是为这三句话生成稠密表示:

  • Mazda CX-9 2018 Review

  • Mazda CX-8 2018 Review

  • Mazda CX-9 2017 Review

接下来,我们制作了这些句子之间的相似度矩阵(基于余弦距离评分)。text-embedding-ada-002 的结果显示,“Mazda CX-9 2018 Review”与“Mazda CX-8 2018 Review”的相似度高于与“Mazda CX-9 2017 Review”的相似度,这不是预期的结果。

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

text-embedding-ada-002 的相似度矩阵(绿色为好,红色为差)

相反,使用 Instructor Large 模型而不使用自定义提示时,“Mazda CX-9 2018 Review”的稠密嵌入与“Mazda CX-9 2017 Review”的相似度高于与“Mazda CX-8 2018 Review”的相似度——这是预期的结果。

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

Instructor Large 的相似度矩阵,未使用自定义提示(绿色为好,红色为差)

当我们加入自定义提示“为检索相关文档生成汽车评论文章问题”时,稠密向量的相似度矩阵显示了改善的结果。你是否注意到“Mazda CX-9 2018 Review”和“Mazda CX-8 2018 Review”之间的相似度评分下降了 0.2%,而“Mazda CX-9 2018 Review”和“Mazda CX-9 2017 Review”之间的评分保持不变?

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

Instructor Large 的相似度矩阵,使用自定义提示(绿色为好,红色为差)

此外,虽然 text-embedding-ada-002 由于其对 Azure 的外部 API 调用需要 1 秒钟的完成时间,但 Instructor Large 的速度较快,在一个合适规模的 EC2 实例上完成时间不到 150 毫秒。这显著提升了我们的整体搜索响应时间。

年份提升

还有一个最后的挑战:当查询没有指定特定汽车模型年份时,我们的文档检索系统没有优先考虑近期文章。解决这个问题很简单。我们决定给较新的文档分配额外的分数。通过精心设计,我们制定了一个二次函数,为较新的文章添加一个轻微的额外分数,随着文章的年龄增加,这个分数逐渐减少。值得注意的是,我们细致地设计了这个函数,以确保近期性的加分不会超过文档和查询之间年份匹配的相关性。这样,即使查询中提到特定年份,我们也避免了一直偏向较新的文章。

通过这些调整,我们成功解决了绝大多数文档检索问题!下面,您将找到最终的架构图。

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

我们最终改进的 RAG 架构

结论

我们有效解决了大部分文档检索难题。在我们的离线测试中,40%的结果更倾向于我们的新解决方案而不是现有的 Google 搜索。我们现在正在将这项技术集成到我们的前端用户体验中,准备进行 A/B 测试,以真正评估这次改进的搜索与现有版本的效率。如前所述,我们也曾面临直接答案的质量问题。虽然我们找到了一个解决方案,但我会在即将发布的博客文章中深入探讨这些细节。

我想传达的一个关键见解是,尽管检索增强生成(RAG)具有巨大的潜力,但它并不是很多文章和帖子所暗示的万能解药,尤其是在最常见的设置下。混合搜索和我们已涵盖的一些其他技术突显出作为增强相关文档检索的有效方法。不断发展的开发和研究领域承诺进一步完善这种方法,使其在未来变得更加稳健。

请通过linkedin与我联系。

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

本文中使用的编辑内容在网上公开可用,并且由 Carsales 拥有。

生成式人工智能负责任使用的紧迫性

原文:towardsdatascience.com/the-urgent-need-for-responsible-use-of-generative-ai-d3134605267f

为什么规模、个性化、来源不明和生成内容的传播要求我们立即行动

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

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

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

照片由 Google DeepMind 提供,来源于 Unsplash

这是什么内容?

“你认为负责任的生成式人工智能(GenAI)为什么重要且紧迫?”这是政策制定者、研究人员、记者和关注的公民们今天提出的问题。生成式人工智能的快速进展吸引了公众的想象力,但也提出了紧迫的伦理问题。像 ChatGPT、Bard 和 Stable Diffusion 这样的模型展示了技术的创造潜力——但在错误的手中,这些相同的能力可能会在前所未有的规模上滋生虚假信息和操控。与以往的技术不同,生成式人工智能使得创造高度个性化、特定背景的合成媒体成为可能,这些媒体很难被验证为虚假。这带来了新的社会风险和复杂的治理挑战。

在这篇博客文章中,我将深入探讨四个方面(规模与速度、个性化、来源不明、传播)来区分这个新纪元的生成式人工智能(GenAI)与以往的时代,并强调为什么现在是关注人工智能伦理和负责任使用的正确时机。在这篇文章中,我旨在通过突出关键方面来回答“为什么是现在?”这个问题。潜在的解决方案将在随后的文章中探讨。

为什么这很重要?

负责任的 GenAI 不仅仅是一个与技术专家相关的假设性问题。这是一个影响我们所有公民的问题,我们在日益复杂的信息生态系统中航行。我们如何在一个我们的眼睛和耳朵可能被欺骗的世界中保持信任和联系?如果任何人都可以制作引人注目却完全虚假的现实,社会如何达成共享的真理?如果不加以控制,GenAI 的滥用将威胁到诚信、同情和人类尊严等基础价值观。但如果我们迅速而集体地实施伦理 AI 设计,我们可以实现生成技术在创造力、联系和社会公益方面的巨大潜力。通过发声和传播意识,我们可以影响 AI 的发展方向。

规模和速度

生成模型使得以惊人的规模、前所未有的速度和简易性创建逼真的虚假内容成为可能。一个人只需简单的提示和点击即可生成无尽的定制音频、视频、图像和文本。这引入了制造操控内容的全新效率和产量水平。人类团队无法与全天候生成无尽定制虚假内容的 AI 系统竞争。只要有足够的计算能力,恶意行为者就可以通过纯粹的人工虚假量淹没社交网络,完全主导真实的声音。随着生成模型变得更易获得和更具说服力,组织大规模虚假信息活动不再需要太多专业知识或资源。

这当然不是一个新现象。例如,Twitter 机器人已经存在相当长时间,它们大约占所有推文的25%,即每天约 2.15 亿条推文。但随着 GenAI 的进步,区分机器人生成内容和人类内容将变得越来越具有挑战性。

个性化

GenAI 可以制作精准利用个体脆弱性和经历的内容。这使得心理操控比泛泛的虚假信息更为强大。设计得极具个人背景共鸣的虚假信息通过破坏共享的真理和现实观念来颠覆人类话语。当任何人都可以被灌输自己独特的一套 AI 制造的“事实”时,社会如何达成共识?这种个性化风险会导致两极化和部落主义,侵蚀群体之间的同情心和联系。

这当然是在即将到来的 2024 年美国大选背景下的热门话题。例如,在 2023 年 5 月,特朗普在他的社交媒体平台 Truth Social 上分享了一段伪造的视频,该视频中是 CNN 主播安德森·库珀。路透社深刻地指出

欢迎来到美国 2024 年总统竞选,这里的现实似乎变得难以把握。

来源

与早期的 Photoshop 等技术不同,通过法医分析验证生成伪造品极为困难。模糊的来源使恶意行为者获得了似是而非的辩解权和侵蚀客观真理的自由。即使是尽职尽责的人也难以实际验证他们遇到的所有生成内容的来源。这种不对称性使得即使生成内容在仔细检查时不够逼真,也能传播虚假信息。

在 2022 年,一段深度伪造的视频被制作出来,似乎显示了乌克兰总统泽连斯基向俄罗斯军队投降。这段视频在社交媒体上广泛传播,导致一些人相信泽连斯基实际上已经投降。视频模糊的来源使得判断其真实性变得困难,即使是尽职尽责的人也很难验证其来源。视频来源的模糊性使得它能够广泛传播,即使有些人可能会在详细查看时质疑其真实性。

扩散

一旦由生成模型制作的高度逼真的伪造品出现,它们可以通过社交网络、消息应用和其他数字平台迅速传播。

尽管这与“规模与速度”部分相关,但从不同角度看这个问题至关重要:深度伪造通常设计得很具情感吸引力。它们可能展示令人震惊、丑闻性的或其他引人注目的内容。这使得它们更容易在社交媒体上被分享,在那里,人们不断寻找新的有趣内容。看到深度伪造的人越多,越有可能有人相信它是真的。即使每个单独的伪造品可能无法欺骗仔细的审查,但大规模传播的数量会压倒追踪和反击虚假信息的努力。病毒式传播赋予生成伪造品一种难以控制的传播范围和影响力。一旦它们“在野外”传播,平台已经难以处理更简单的虚假信息——由生成 AI 创建的内容则更高难度。

举一个具体的例子:在 2023 年 3 月,一张 AI 生成的教皇方济各的照片在社交媒体上病毒式传播,一条推文获得了近 2100 万次观看——它甚至获得了“巴伦西亚教皇”的昵称。根据纽约邮报的报道,创作该图像的艺术家对这些关注并不感兴趣,恰恰相反:

被指生成该图片的 AI 艺术家 Pablo Xavier 表示,他“并不希望[这些图片]如此轰动”,并承认“人们在不加质疑的情况下认为它是真的,这确实令人害怕。”

结论

这些前所未有的能力——规模与速度、个性化、模糊来源和扩散——从根本上改变了虚假信息的性质,迫切需要辩论。我们如何应对一种能够大规模操控人们并摧毁共识现实的技术?需要什么治理措施才能在在线话语中保持信任和真相?我们能否遏制生成 AI 的有害应用,同时培养有益的应用?没有简单的答案,但现在进行诚恳的讨论对将这项技术引向伦理结果至关重要。

在一个由人工智能驱动的世界中,维护人类自主权、尊严和我们共享的现实是至关重要的。随着生成模型变得越来越强大和易于获得,我们需要伦理性护栏和智能治理,以防止反乌托邦的结果。必须迅速而深思熟虑地采取行动——在假设性风险通过自动化和人工智能操控扩散成现实之前。现在是塑造生成 AI 未来的最佳时机,以实现公正和有益的方向。

Heiko Hotz

👋 在MediumLinkedIn关注我,阅读更多关于生成 AI、机器学习和自然语言处理的内容。

👥 如果你在伦敦,请加入我们的NLP London Meetups

📔 我对 AI 新闻的看法在😇 Naughty Neural

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

作者提供的图片

正则表达式在数据科学中的实用性

原文:towardsdatascience.com/the-utility-of-regular-expressions-in-data-science-babf94285966

Python 中常见应用的示例

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

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

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

图片由 Kevin Ku 提供,来源于 Unsplash

介绍

数据科学家经常发现自己需要确定数据中的特定字段是否符合所需的文本格式,或特定字符串是否存在。在其他情况下,他们可能需要将数据中的特定字符串替换为另一个。为实现这一点,他们使用已成为这类问题常规的正则表达式

本文将简要介绍什么是正则表达式,介绍一些形成相应搜索模式所需的基本字符,展示一些 Python 中常用的函数,并最后讨论一些数据科学家日常生活中经常遇到的实际使用案例。

什么是正则表达式?

正则表达式,或称 Regex,是一组字符,用于启用特定文本模式的搜索和——如果需要——替换。这是一种极其方便的技术,数据科学家可以利用它来避免繁琐的手动搜索任务。

为了定义这些搜索模式,必须熟悉相应的语法。Dataquest 提供了一个全面且简洁的 Python 正则表达式语法备忘单。虽然本文不会涵盖所有使用案例,但仍值得强调一些用于定义搜索模式的基本字符(见表 1)。

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

表 1:定义正则表达式搜索模式的基本字符。作者提供的图示,灵感来源于 Dataquest

Python 中的正则表达式

Python 提供了一个名为re的模块,该模块提供了一整套丰富的正则表达式匹配操作。在这里,我们将重点介绍四个常用的模块函数:

  1. 搜索: 匹配表达式的第一个实例。

  2. 查找所有: 匹配表达式的所有实例。

  3. 替换: 用另一个字符串替换指定的字符串。

  4. 拆分: 根据指定的分隔符拆分字符串。

让我们通过一些具体的例子来进一步说明这些概念:

import re

# Defining the string upon which we'll run some regex operations
s = "Paris is the capital of France. Also, Paris is a beautiful city."

# Search
re.search("Paris", s)
>>> <re.Match object; span=(0, 5), match='Paris'>

# Find All
re.findall("Paris", s)
>>> ['Paris', 'Paris']

# Sub
re.sub("Paris", "Rome", s)
>>> 'Rome is the capital of France. Also, Rome is a beautiful city.'

# Split
re.split("\. ", s)
>>> ['Paris is the capital of France', 'Also, Paris is a beautiful city.']

正如预期的那样,search 函数只返回表达式的第一个实例作为 re.Match 对象。另一方面,findall 函数匹配表达式的所有实例并将它们以列表形式返回。使用 sub 函数,可以将特定字符串替换为另一个字符串——在这种情况下,Paris 被替换为 Rome。最后,可以使用 split 函数并指定特定的分隔符来拆分字符串——在这种情况下,是句点符号。请注意,我们必须在句点前使用转义字符 \ 来转义它在正则表达式中的特殊含义。

数据科学中的实用性

基于模式的搜索是数据科学家经常遇到的一个概念。让我们通过一些例子来突显将正则表达式作为工具放入工具箱中的实用性。

验证文本格式

一个相当常见的场景是表格数据的分析,其任务是检查某一列中的字段是否符合所需的格式。假设我们在一个名为 sample.csv 的文件中有一些虚构数据,该文件包含以下列:电子邮件地址、城市、州和邮政编码(表 2)。

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

表 2:包含电子邮件和地理信息的表格化的虚构数据。

现在,我们希望验证所有电子邮件地址是否符合 firstname.lastname@gmail.com 的格式。利用正则表达式,我们可以进行以下操作:

import re
import pandas as pd

df = pd.read_csv("sample.csv")

# Define pattern that matches the email format firstname.lastname@gmail.com
pattern = "^[a-z]+\.+[a-z]+@gmail.com$"

# Iterate through rows and print those that do not match
for index, row in df.iterrows():
    match = re.search(pattern, row.Email)
    if match == None:
        print(index, *row)

# Output
>>> 0 johnsmith@gmail.com Pasadena California 91103
>>> 3 matt.hawkins@hotmail.com Kailua Hawaii 96734

如果模式与字符串输入不匹配,search 函数将返回 None。如果我们打印出这些场景,我们可以立即发现第 0 行的电子邮件地址的名字和姓氏没有用句点分隔,而第 3 行的电子邮件地址使用了 Hotmail 而不是 Gmail。

以类似的方式,我们可以验证数据中的邮政编码是否符合标准的五位数字格式。

# Define pattern that matches 5-character strings comprised of all digits
pattern = "^\d{5}$" 

for index, row in df.iterrows():
    match = re.search(pattern, row.ZIP)
    if match == None:
        print(index, *row)

# Output
>>> 2 Kirkland Washington 9560

我们可以看到在第 2 行,邮政编码是错误的,因为它只有四位数字。

相同的原理可以应用于各种格式特定的字段输入,如日期、网址、电话号码、IP 地址、社会安全号码等。

检测模糊词

可以对文本进行分析,以查找某些短语、单词或其模糊形式。例如,垃圾邮件发送者倾向于模糊特定的单词,希望绕过垃圾邮件过滤器。例如,可以使用j@ckpot, jackp0t,j@ackp0t来代替jackpot。正则表达式可以确保不仅捕获感兴趣的单词,还能捕获一些其模糊形式:

import re

# Define pattern capable of handling some obfuscations of "jackpot"
pattern = "^j[a@]ckp[o0]t$"

re.search(pattern, "j@ckpot")
re.search(pattern, "jackp0t")
re.search(pattern, "j@ckp0t")

# Outputs
>>> <re.Match object; span=(0, 7), match='j@ckpot'>
>>> <re.Match object; span=(0, 7), match='jackp0t'>
>>> <re.Match object; span=(0, 7), match='j@ckp0t'>

在这里,我们定义了一组字符,用于在特定位置进行匹配,例如位置 1 的[a@]和位置 5 的[o0]。因此,我们上面提到的所有jackpot的模糊匹配都将被识别。

正确的格式或拼写

假设我们有一些文本数据,其中包含以美国格式表示的日期,即MM/DD/YYYY。然而,我们希望将这些数据中的所有日期转换为更广泛使用的格式DD/MM/YYYY。同样,可以应用正则表达式来解决这个问题:

import re

string = "I have a dentist appointment on 04/06/2023 at 10:00 AM."

# Define pattern capable of capturing data format
pattern = "(\d{1,2}/)(\d{1,2}/)(\d{4})"

# Substitute days and months
re.sub(pattern, r"\2\1\3", string)

# Output
>>> 'I have a dentist appointment on 06/04/2023 at 10:00 AM.'

我们成功地将04/06/2023替换为06/04/2023。具体来说,我们使用( )对对应于月份(组 1)、日期(组 2)和年份(组 3)的表达式进行分组。为了交换月份和日期,我们只需在替换参数中指定所需的组顺序——在我们的例子中是r"\2\1\3"。请注意,我们在这里使用原始字符串表示法,以避免在没有r前缀的字符串字面量中出现的反斜杠特殊处理。

类似的方法可以用于纠正文本中的拼写错误,例如将gray替换为grey,或反之。

计算生物学中的应用

另一个广泛使用正则表达式的领域是计算生物学。蛋白质由一串氨基酸组成,这些氨基酸在折叠成最终的三维结构后,通常在我们体内发挥特定的功能。检测特定结构模式的存在或缺失——一种与特定功能相关的短氨基酸模式——对于更好地理解其最终功能可能至关重要。

假设我们有一组短蛋白质,我们希望找到那些包含以下模式的蛋白质:xC[DA]GG{Y}。这是一种生物学符号,解释如下:任何氨基酸(x),后跟半胱氨酸(C),后跟天冬氨酸(D)或丙氨酸(A),后跟两个连续的甘氨酸(G),后跟任何氨基酸但不包括酪氨酸(Y)。使用 Python 中的正则表达式,我们可以检测到包含此模式的蛋白质,如下所示:

import re

# Define pattern capable of matching the above-mentioned motif
pattern = ".C[DA]GG[^Y]"

proteins = ['AARKYL', 'LELCDGGPG', 'RAAANCDD', 'LYYRCAGGEGPGG', 'CAEELR']  

for prot in proteins:
    match = re.search(p, prot)
    if match:
        print(prot, match)

# Output
>>> LELCDGGPG <re.Match object; span=(2, 8), match='LCDGGP'>
>>> LYYRCAGGEGPGG <re.Match object; span=(3, 9), match='RCAGGE'>

这种方法可以正确识别出含有我们寻找的特定结构的两个蛋白质。

结论

正如我们所看到的,正则表达式在各种学科中都非常有用,从电子邮件垃圾过滤器到计算生物学。本文仅展示了正则表达式常见应用的几个场景,但其适用范围实际上非常广泛。尽管正则表达式有其局限性,并且肯定不是解决所有文本处理问题的万灵药,但它们应该被视为每个数据科学家工具箱中的基本工具。

参考文献

[1] 数据科学备忘单。Dataquest。检索日期:2023 年 1 月 4 日,网址:www.dataquest.io/wp-content/uploads/2019/03/python-regular-expressions-cheat-sheet.pdf

[2] 正则表达式操作。Python。检索日期:2023 年 1 月 4 日,网址:docs.python.org/3/library/re.html

使用 Q-learning 的强化学习中行动的价值

原文:towardsdatascience.com/the-values-of-actions-in-reinforcement-learning-using-q-learning-cb4b03be5c81

使用 Python 从头实现的 Q-learning 算法

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

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

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

代理在迷宫中穿越;GIF 由作者提供

这篇文章是关于强化学习(RL)系列文章的续集。请在这里查看其他文章:

## 使用 Python 进行强化学习的第一步

如何在强化学习的基本世界之一中找到最佳位置的原始 Python 实现…

towardsdatascience.com ## 使用 Python 的时间差分 — 第一个基于样本的强化学习算法

使用 Python 编写和理解 TD(0) 算法

towardsdatascience.com

所有使用的代码可以在这里查看:github.com/Eligijus112/rl-snake-game

包含所有绘图函数和代理训练代码的笔记本可以在这里查看:github.com/Eligijus112/rl-snake-game/blob/master/chapter-6-qlearning.ipynb

在这篇文章中,我将向读者介绍 Q 值的概念。为了直观起见,读者可以将 Q 替换为 Quality。q 值是数值,分配给 每个动作每个状态 中的得分:

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

Q 值函数

在给定状态下,某个特定动作的 得分越高,代理执行该动作的 效果越好

例如,如果我们可以从状态 1 选择左移或右移,那么如果

Q(left, 1) = 3.187

Q(right, 1) = 6.588

那么从状态 1 出发,能够带来更多价值的更好动作就是“正确”的动作。

存储 q 值的对象是q-tableq-table 是一个矩阵,每一行是一个状态,每一列是一个动作。我们将这种矩阵称为Q

从之前的文章中,让我们回顾一下 Q 学习中需要的一些其他重要表格:

S — 状态矩阵,用于索引所有状态。

R — 奖励矩阵,指示过渡到给定状态时获得的奖励。

Q 学习中不需要价值函数 V,因为我们不仅关心状态的价值,还关心状态-动作对的价值。

想象一下我们有以下 48 状态的迷宫:

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

迷宫;作者拍摄

黄色状态是我们代理的起始状态(状态 1)。

绿色状态是目标状态(状态 38)。

红色状态是迷宫的墙壁。如果代理选择去墙壁状态,它将返回到最后一个状态且不获得奖励。离开边界的逻辑也适用。

我们的代理可以采取的动作由向量 [0, 1, 2, 3] 表示,对应于 [上, 下, 左, 右]

该代理的初始 Q 表如下:

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

48x4 矩阵;作者拍摄

有 48 行表示每个状态。

有 4 列表示代理在每一步可以采取的 4 种动作:上、下、左或右

Q 学习算法的主要目标是 填充上述矩阵,以便我们的代理学习迷宫中最优的路径

我们将使用自定义的 Agent 类来实现 Q 学习算法:

class Agent:
    def __init__(
        self, 
        nrow_maze: int,
        ncol_maze: int,
        actions: list = [0, 1, 2, 3],
        rewards: dict = {
            'step': 0.0, 
            'wall': 0.0,
            'goal': 10,
        }, 
        gamma: float = 0.9,
        alpha: float = 0.1,
        epsilon: float = 0.1,
        seed: int = 42,
        ) -> None:
        """
        Creates an agent for the maze environment.

        Parameters
        ----------
        nrow_maze : int
            The number of rows in the maze.
        ncol_maze : int
            The number of columns in the maze.
        actions : list, optional
            A list of actions that the agent can take. The default is [0, 1, 2, 3]. 
            0: Up
            1: Down
            2: Left
            3: Right
        rewards : dict, optional
            A dictionary of rewards for the agent. The default is {'step': -1, 'wall': -10, 'goal': 10}.
        gamma : float, optional
            The discount factor. The default is 0.9.
        alpha : float, optional
            The learning rate. The default is 0.1.
        epsilon : float, optional
            The exploration rate. The default is 0.1.
        seed : int, optional
            The seed for the random generator. The default is 42.
        """
        self.nrow_maze = nrow_maze
        self.ncol_maze = ncol_maze
        self.rewards = rewards
        self.gamma = gamma
        self.alpha = alpha
        self.epsilon = epsilon
        self.seed = seed
        self.actions = actions

        # By default, the starting index is 0 0 
        self.start_state = 0

        # By default, the goal index is the last index
        self.goal_state = nrow_maze * ncol_maze - 1

        # Creating the random generator with a fixed seed
        self.random_generator = np.random.default_rng(seed)

        # Creating the maze; We will denote it internaly as S 
        self.init_S_table()

        # Initiating the Q-table 
        self.init_Q_table()

        # Saving the initial past_action and past_state
        self.past_action = None
        self.past_state = None

        # Creating the action name dictionary 
        self.action_name_dict = {
            0: 'up',
            1: 'down',
            2: 'left',
            3: 'right',
        }

        # Counter for the number of times our agent has seen the terminal state
        self.num_goal_reached = 0

        # Counter for each state and how many times the agent visited each 
        self.state_visit_counter = {}

        # Empty dictionary of states visition paths
        self.state_visit_paths = {}

        # Placeholder for the current episode of learning 
        self.current_episode = 0

  #####
  # OTHER METHODS BELLOW
  #####

完整的 Q 学习算法如下¹

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

Q 学习算法;作者拍摄

在步骤 2.2.1 中的 epsilon-贪婪策略 是以 1 - epsilon 的概率采取 Q 值最大的动作,并以 epsilon 的概率采取随机动作。

上述策略通过以下代码在我们的代理中实现:

def argmax(self, q_values: np.array):
        """argmax with random tie-breaking
        Args:
            q_values (Numpy array): the array of action values
        Returns:
            action (int): an action with the highest value
        """
        top = float("-inf")
        ties = []

        for i in range(len(q_values)):
            if q_values[i] > top:
                top = q_values[i]
                ties = []

            if q_values[i] == top:
                ties.append(i)

        return self.random_generator.choice(ties)

    def get_greedy_action(self, state: int) -> int:
        """
        Returns the greedy action given the current state
        """
        # Getting the q values for the current state
        q_values = self.Q[state]

        # Getting the greedy action
        greedy_action = self.argmax(q_values)

        # Returning the greedy action
        return greedy_action

    def get_epsilon_greedy_action(self, state: int) -> int: 
        """
        Returns an epsilon greedy action
        """
        if self.random_generator.random() < self.epsilon:
            return self.get_action()
        else:
            return self.get_greedy_action(state)

Q 学习步骤是 2.2.3 步骤。在每个状态下,我们的智能体采取一个动作。然后,通过更新智能体所在的当前状态和状态-动作对**Q(S, A)**来完成学习。更新规则中最重要的部分是,我们查看智能体通过采取动作最终到达的状态,然后从 Q 表中提取该状态的最大值。

让我们更仔细地检查这个方程:

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

Q 值更新方程

Q(S, A)是智能体所在的状态以及他采取的动作。

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

转换状态的最大值

部分是我们智能体在所有动作中最终到达的状态的最大可用 Q 值。

r 是过渡到给定状态的奖励。

其他所有的是用户定义的超参数。

由于我们使用算法估计值来更新 Q 值,因此 Q 学习属于自举方法家族。

每次我们的智能体移动后,Q 表都会更新。

我们在 Agent 类中 2.2.3 步骤的完整实现如下:

def update_Q_table(self, new_state: int): 
        """
        Function that applies the RL update function
        """ 
        # Getting the next_state's reward
        reward = self.reward_dict[new_state]

        # Saving the current Q value
        current_Q = self.Q[self.past_state][self.past_action]

        # If the new state is the terminal state or the wall state, then the max_Q is 0
        max_Q = 0

        # Else we get the max Q value for the new state
        if new_state != self.goal_state:
            new_state_Q_values = self.Q[new_state]

            # Getting the max Q value 
            max_Q = np.max(new_state_Q_values)

        # Updating inplace the Q value 
        self.Q[self.past_state][self.past_action] = current_Q + self.alpha * (reward + self.gamma * max_Q - current_Q)

上述函数在每次智能体移动时都会被调用:

 def terminal_step(self, new_state: int):
        """
        Updates the agent one last time and resets the agent to the starting position
        """ 
        # Updating the Q table
        self.update_Q_table(new_state)

        # Resetting the agent
        self.past_state = self.start_state
        self.past_action = self.get_epsilon_greedy_action(self.past_state)

        # Incrementing the number of episodes
        self.current_episode += 1

    def get_next_state(self, s: int, action: int) -> int: 
          """
          Given the current state and the current action, returns the next state index
          """
          # Getting the state coordinates
          s_row, s_col = self.get_state_coords(s)

          # Setting the boolean indicating that we have reached the terminal state 
          reached_terminal = False

          # Getting the next state
          next_state = -1
          if action == 0:
              next_state = self.get_state_index(s_row - 1, s_col)
          elif action == 1:
              next_state = self.get_state_index(s_row + 1, s_col)
          elif action == 2:
              next_state = self.get_state_index(s_row, s_col - 1)
          elif action == 3:
              next_state = self.get_state_index(s_row, s_col + 1)

          # If next_state is a wall or the agent is out of bounds, we will stay in the same state
          if (next_state == -1) or (next_state in self.wall_states):
              return s, reached_terminal

          # If next_state is the goal state, we will return to the starting state
          if next_state == self.goal_state:
              # Incrementing the number of times our agent has reached the goal state
              self.num_goal_reached += 1
              reached_terminal = True

          # Returning the next state
          return next_state, reached_terminal

    def move_agent(self): 
        """ 
        The function that moves the agent to the next state
        """
        # Getting the next state
        next_state, reached_terminal = self.get_next_state(self.past_state, self.past_action)

        # Updating the Q table
        if not reached_terminal:
            # Checking if the past_state is the same as the next_state; If that is true, it means our agent hit a wall 
            # or went out of bounds
            if self.past_state != next_state:
                self.update_Q_table(next_state)

            # Setting the past_state as the next_state
            self.past_state = next_state

            # Getting the next action
            self.past_action = self.get_epsilon_greedy_action(self.past_state)
        else: 
            self.terminal_step(next_state)

上面的代码片段应从下往上阅读。

在每次移动时,我们检查是否处于终止状态。如果智能体进入终止状态,Q 学习更新方程简化为:

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

终止状态的更新;作者提供的照片

让我们启动我们的智能体,训练一个回合并可视化智能体路径:

 def train_episodes(self, num_episodes: int):
      """
      Function that trains the agent for one episode
      """
      # Calculating the episode number to end the training 
      end_episode = self.current_episode + num_episodes - 1

      # Moving the agent until we reach the goal state
      while self.current_episode != end_episode:
          self.move_agent()
# Creating an agent object
agent = Agent(
    nrow_maze=6,
    ncol_maze=8,
    seed=6,
    rewards={'step': 0, 'goal': 10}
)

# Initiating the maze 
agent.init_maze(maze_density=11)

# Training the agent for one episode
agent.train_episodes(num_episodes=1)

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

智能体在迷宫中游荡;作者提供的 GIF

智能体需要 94 步才能到达目标。在每一步,智能体以 epsilon 贪婪的方式选择一个动作。在第一次迭代中,任何转换状态的 Q 值都是 0,因此 epsilon 贪婪算法与随机游荡相同。

让我们在一个回合后检查 Q 表。Q 表中所有的值都是零,只有状态 30 的值例外:

agent.Q[30]

# Returns 
# array([0., 1., 0., 0.])

Q(30, 1)(意味着从状态 30 向“下”移动)值为 1。计算这个值的方程是:

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

请记住,初始Q(30, 1) = 0.

在一个回合后,我们只学到了一个 Q 值。让我们再训练一个回合:

# Training the agent for one episode
agent.train_episodes(num_episodes=1)

# Printing out the agent's 37 state
agent.Q[37]

# Returns 
# array([0., 0., 0., 1.])

现在智能体从迷宫的另一边游荡,学到从状态 37 向右移动是最佳选择。

我们希望看到的是,随着回合的进行,我们的智能体的步骤数开始减少。

# Creating an agent object
agent = Agent(
    nrow_maze=6,
    ncol_maze=8,
    seed=6,
    rewards={'step': 0, 'goal': 10}
)

# Initiating the maze 
agent.init_maze(maze_density=11)

# Letting the agent wonder for 1000 episodes
agent.train_episodes(100)
state_visits = agent.state_visit_paths
steps = [len(state_visits[episode]) for episode in state_visits]

# Ploting the number of steps per episode
plt.plot(steps)
plt.title("Number of steps per episode")
plt.xlabel("Episode")
plt.ylabel("Number of steps")
plt.show()

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

步骤数与回合数的关系;作者提供的照片

在初步探索后,到第 20 集时,代理拥有一个稳定的策略,并需要大约 10 步从起始位置到达终点位置。变异发生是因为我们仍在使用 epsilon 贪婪算法进行移动,而 10%的时间会选择随机动作。

现在,最佳贪婪策略如下所示:

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

最优策略;作者拍摄的照片。

此外,我们的代理跟踪它在任何给定状态下出现的次数。我们可以绘制这些数据,以查看在训练阶段哪些状态最受欢迎:

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

状态访问;作者拍摄的照片

我们可以看到,代理经常在起始状态的左右徘徊。但是,由于我们让代理只有 10%的时间采取随机动作,因此主要路径是贪婪的,也就是说,代理选择具有最大 Q 值的动作。

最后,我们可以绘制最终的代理遍历路径:

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

代理遍历;作者制作的 GIF

总结:

  • Q 学习算法在代理执行每个动作后更新 Q 表中的值。

  • Q 学习是一种自举算法,因为它使用自己的估计来更新 Q 值。

  • 在 Q 学习中,我们只需要状态、奖励和 Q 表来实现整个算法。

  • Q 学习中的主要更新规则是:

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

Q 值更新方程

编程愉快,学习愉快!

[1]

  • 作者:理查德·S·萨顿,安德鲁·G·巴托

  • 年份:2018

  • 页码:131

  • 标题:强化学习:导论

  • 网址:http://archive.ics.uci.edu/ml

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值