XGBoost 如何原生支持类别特征?
原文:
towardsdatascience.com/native-support-of-categories-in-xgboost-how-does-it-work-d359096bd003
·发表于Towards Data Science ·6 分钟阅读·2023 年 3 月 22 日
–
XGBoost 和其他基于决策树的方法通过梯度提升进行训练,使用比较来做出决策。为类别定义一个比较操作符在数学上并不简单。
在这篇文章中,我们将解释可用的选项,详细说明它们的优缺点,并重点关注最近在 XGBoost(以及 LightGBM)中引入的对分类特征的原生支持。
如果你对梯度提升及其在决策树训练中的应用感兴趣,请考虑我的书:
这本关于梯度提升方法的书面向希望深入了解的学生、学者、工程师和数据科学家……
amzn.to](https://amzn.to/3LDmbKM?source=post_page-----d359096bd003--------------------------------)
决策树
如下图所示,决策树基于比较来做出决策:
一个简单的决策树。图由作者提供。
例如,在这个简单的例子中,如果输入是一行数据,包含两列A=21
和B=111
,则输出值将是权重4
。
这种决策树的局限性在于它只能处理数字作为特征。
处理分类特征的标准方法
然而,在数据科学中,经常会遇到分类特征。让我们看看现有的选项。
一热编码
处理类别值的一种常见方法是使用 独热编码。这里的想法是通过为每个可能的类别创建一列,将非数值类别值转换为数值值。
当类别适用于数据集的当前行时,相应的列被设置为 1,否则为 0。
以下代码片段展示了使用 pandas 进行独热编码的标准方法:
使用 Pandas 进行独热编码。由作者编写的代码。
独热编码的主要限制之一是,你将为数据集中每个不同的类别值添加相应的列。
同样重要的是,在训练和预测时必须使用相同的离散值,否则在预测过程中会缺少某些列。
GLMM 编码
另一种选择是使用编码,更具体地说是 GLMM 编码。这里的想法是通过模型将非数值类别转换为数字。
转换是通过 GLMM 完成的,即 广义线性混合模型。
这里 广义 表示 GLMM 只是线性模型的一种推广,其中目标变量可以通过对数等函数进行变换。因此,利用相同的数学框架,可以对标准线性回归和逻辑回归进行建模。
而 混合 表示这些模型可以整合固定效应和随机效应,即预测变量的均值在一组观测中要么是固定的,要么是随机的。
应用于类别编码的情况时,GLMM 将捕捉每个类别的随机效应,并适配这种模型:
混合模型预测类别 I 的 Y。由作者提供的公式。
其中 Y_i
是 Mixel 模型为目标 Y
和类别 i
预测的值。μ 是 Y
的全局均值,表示固定效应,而 RE_ci
是由于类别 i
引起的随机效应。
因此 RE_ci
捕捉了类别的效应,这就是类别的编码值。
使用 pandas
和 categorical_encoders
库,所有这些理论可以归纳为以下几行代码:
GLMM 编码与 Pandas。由作者编写的代码。
如果你感兴趣,categorical_encoders
库中的重要代码行是:
由作者编写的代码。
这正好训练了一个由上面的公式描述的模型。
XGBoost 原生支持
如上所述,可以通过将类别值转换为实际值(glmm 编码)或通过更改数据集结构并为每个可能的类别引入一列(独热编码)来改变类别值的属性。
但这似乎不是在标准的梯度提升结构中整合类别支持的最佳方式:决策树。
包括而非比较
确实,似乎自然地将比较替换为包含作为决策操作。即,与检查一个值是否大于或小于给定阈值不同,我们可以检查该值是否在给定的类别集合中。
这意味着我们可以用一个阈值来替代比较。
作者公式
作者
作者公式
基于我上一篇文章中的代码:
## DIY XGBoost 库,少于 200 行 Python
XGBoost 解释以及梯度提升方法和超参数调整,通过构建你自己的梯度提升库来实现…
towardsdatascience.com
这可以通过更新创建节点条件函数的方法来实现,以支持包含而不是比较:
使用类别时创建一个包含条件。作者代码。
穷尽生成可能的类别列表
在标准方法中,使用值作为分割条件,通过对考虑的列中的值进行排序并保持唯一值来生成候选分割。
根据这个分割值,评估左节点和右节点的增益。
当使用类别时,排序不再相关,候选分割条件不再是单一值,而是类别列表。
由于我们对优化增益的最佳类别分组方式没有先验知识,一种选择是生成所有可能的类别组合。
因此,根据我上一篇文章中的代码,我们提取了创建基于阈值的数值分割的_numerical_split
代码,并引入了一个新方法_categorical_split
来生成可能的组合:
为数值和类别类型生成候选分割。作者代码。
请注意,pandas 掩码是一种非常方便的方式来类似地处理这两种情况。
将所有内容汇总
完整代码,从头实现了决策树的梯度提升,并支持类别数据和数值数据,见下文:
作者的完整代码。
除了上述两个主要更改外,代码基本与我上一篇文章中的相同,但稍微更通用,以便同时支持数值数据和类别数据。
在提供的基本示例中,使用类别分割或数值分割得到相同的结果。当使用类别支持时,代码只会稍微慢一些,因为探索的组合数量很重要:
探索的组合。作者代码。
请注意,我们使用内部 pandas 代码 category
来区分数值数据和分类数据。
性能问题
上述实现按预期工作,并支持分类值。
然而,由于我们正在全面探索所有可能的类别组合,使用 python itertools.combinations
时,当唯一类别超过几个时,代码会变得非常慢。
实际上,对于一组 n
个分类值,k
的可能组合数由以下公式给出:
可能的组合数。公式由作者提供。
这就是为什么 XGBoost 和 LightGBM 使用启发式方法来大幅减少这个数量。更多细节可以在这里找到:
注意:截至 XGBoost 1.6,此功能仍为实验性,功能有限。从 1.5 版本开始,XGBoost 具备了…
xgboost.readthedocs.io](https://xgboost.readthedocs.io/en/stable/tutorials/categorical.html?source=post_page-----d359096bd003--------------------------------#optimal-partitioning)
www.buymeacoffee.com/guillaumes0
结论
我强烈建议在使用 XGBoost 或 LightGBM 时使用对分类数据的内部支持。这不仅是一种更高效的内存和计算方法,而且提供了良好的精度。
它还简化了数据准备流程,并在训练和预测集不重叠时原生支持缺失值。
如果你对梯度提升及其在决策树中的应用感兴趣,请考虑阅读我的书:
这本关于梯度提升方法的书旨在为希望…
amzn.to](https://amzn.to/3LDmbKM?source=post_page-----d359096bd003--------------------------------) [## Guillaume Saupin
Guillaume Saupin,博士学位持有者及应用数学专家,在数学界因其…
www.buymeacoffee.com](https://www.buymeacoffee.com/guillaumes0?source=post_page-----d359096bd003--------------------------------)
使用 Spotify 的 Pedalboard 进行自然音频数据增强
原文:
towardsdatascience.com/natural-audio-data-augmentation-using-spotifys-pedalboard-212ea59d39ce
包含现成的 Python 代码和预设
·发表于Towards Data Science ·10 分钟阅读·2023 年 1 月 9 日
–
图片改编自户山 神奈.
作为数据科学家,处理音频数据常常是一项艰巨的任务。一个常见的问题是数据缺乏,这会影响构建有效模型(如语音识别、音乐推荐和语音转文本)的能力。通常,音频数据是敏感的,可能包含人声或受版权保护的音乐。最后,相比其他数据类型,音频数据是高维的,通常需要巨大的计算资源。因此,充分利用现有数据至关重要。音频数据增强是一种很好的方法来实现这一目标!
在本文中,我们将讨论以下主题:
-
(音频)数据增强基础
-
Spotify Pedalboard 简介
-
音频数据增强的最佳实践
-
利用现成代码和预设的高级用法
数据增强
数据增强是通过轻微修改数据集中部分或所有示例来增加训练数据总量的过程。如果做得正确,增强数据集可以有效地防止过拟合。这意味着,即使数据量相对较小,你的机器学习模型也很可能会产生更具普遍适用性的结果。数据增强通常仅应用于训练数据,因为如果将增强混入验证和测试数据中,度量指标将变得无效。
最重要的是,增强示例必须仍然足够类似于原始示例,以便被视为*“同一类别”*。这就是我们所说的自然或分布内增强。以下是图像识别中的一个示例,数据增强被广泛应用:如果你旋转和平移猫的图像,你可以生成各种经过修改的图像,这些图像在技术上是“不同”的,但仍然明显是猫。
为数据增强而对猫的图像进行平移和旋转,这一灵感来源于 Suki Lau 的介绍文章。猫的图像由Alexander London提供。
如果你的增强示例看起来在模型部署时可能会遇到“现实世界”的示例,那就可以了。
这一经验法则很有用,但这对音频数据意味着什么呢?
音频数据增强
我有个好消息要告诉你:在机器学习中,音频数据通常表现为图像,例如谱图。那么它与猫的例子有多大不同呢?嗯,这里也有一个坏消息:平移或旋转这些图像都没有用。只需看看下面的图像:
应用在谱图上的旋转和平移。图像由作者提供。
你不需要任何信号处理方面的专业知识就能看出,平移和旋转后的谱图不是自然增强。你的模型在“现实生活”中永远不会看到平移/旋转的谱图,因此你不应使用这些进行训练。
有三种音频数据增强方法被广泛采用:
-
在将音频转换为图像表示后,使用遮罩来“隐藏”图像中的某些部分。
-
如果你的音频文件比你想用作模型输入的长度要长,你可以从单个音频文件中提取多个片段。
-
对你的示例应用信号修改,如音频效果,以稍微改变它们。
在这三种方法中,我认为第三种选项最好,它能够可靠地产生所有类型音频数据的自然增强。然而,它需要大量的努力和细致的思考才能正确完成。这就是为什么我把它作为本文的主题。
无论是语音还是音乐,声音文件通常可以用音频效果如压缩器、均衡器或混响来编辑。实际上,这些是任何声音工程师的“基本功”。好消息是,这些效果在我们处理的数据中非常固有,因此特别适合数据增强。如果我增加了摇滚歌曲的整体混响,它仍然是一首摇滚歌曲,但生成的谱图可能看起来明显不同。
踏板板
吉他踏板板。图像由Frankie Lopez提供。
Pedalboard 于 2021 年 9 月相对较新发布,附有 介绍文章 和 github 仓库。术语 “pedalboard” 源于电吉他手,他们通常使用多个吉他效果器,比如失真或混响,通常以脚踏板的形式连接到效果板上。尽管 pedalboard 显然是为音乐应用而构建的,但它也非常适合语音增强。
安装
你可以在终端中运行以下命令来安装 pedalboard。
pip install pedalboard
要使用 Pedalboard 的效果,你需要将音频文件加载到 Python 中,作为其波形的数组表示形式。“librosa”和“soundfile” 在这方面尤其有用。
pip install librosa
pip install soundfile
为了使 librosa 正常工作,你还需要下载并安装 FFmpeg 并 将其添加到你的 PATH 环境变量** 中**。
加载音频
要加载音频文件,只需使用:
import librosa
audio, sr = librosa.load("path/to/file")
“librosa.load” 返回波形数组和音频采样率。我们将使用以下短音乐示例贯穿本文,以演示 pedalboard 的功能:
这是我在不久前制作的一段短小的放克风格的器乐片段。如果你想跟随操作,可以直接从 SoundCloud 下载音频文件。
应用单个效果踏板
首先,我们从 pedalboard 库中导入 “Pedalboard” 和 “Reverb”。即使我们只想应用一个效果,也需要首先实例化一个 “Pedalboard”,然后将我们的效果添加到其中。作为效果,我们将使用具有默认 pedalboard 设置的混响效果。
from pedalboard import Pedalboard, Reverb
# Create a pedalboard with one pedal
board = Pedalboard()
board.append(Reverb()) # instantiate with default parameters
# Apply pedal to audio signal
audio_reverbed = board(audio, sr)", audio_reverbed, sr)
有许多方法可以将音频波形导出为 .mp3 或 .wav 文件。这里是一种常见的方法:
import soundfile as sf
sf.write("path/to/file.wav", audio_reverbed, sr)
这是应用标准混响效果后的示例声音:
很容易听出,尽管混响效果已经相当强烈,但我们保留了相关的音乐参数和整体的曲目表现。
如果我们想要不同类型的混响效果怎么办?Pedalboard 允许我们为每个效果踏板使用自定义参数。例如,这里是如何创建一个比之前使用的混响效果更为突出的效果:
board = Pedalboard()
board.append(Reverb(room_size=0.15, damping=0.8, wet_level=0.9))
audio_reverbed = board(audio, sr)
如果你听下面的示例,你会发现混响效果非常突出,以至于掩盖了基础音乐。这是一个不自然增强的例子,至少对于大多数使用情况来说是这样。
应用多个效果踏板
我们可以使用更多效果并将它们组合成一个效果链。例如,PitchShift 会将整个音频的音高提高或降低,比如提升两个半音。Compressor 会将信号中较响的部分降低,从而使声音的动态范围减少,更加紧凑。除此之外,我们还可以再次添加具有自定义参数的混响效果。
from pedalboard import Pedalboard, Compressor, PitchShift, Reverb
# Create board with multiple pedals
board = Pedalboard(
[
PitchShift(semitones=2),
Compressor(threshold_db=-20, ratio=2),
Reverb(wet_level=0.3)
]
)
# Apply board
audio_multieffect = board(audio, sr)
这是结果音频的声音示例:
我们是否已经对效果处理过度了?当然,这需要在机器学习任务的背景下讨论。如果我想检测歌曲的调性,显然我们不应该改变音高,以保持相同的音高标签。如果我们尝试检测一首曲目的风格,它仍然可以明显被识别为——制作不佳的——放克曲目。
问题和最佳实践
图片改编自 Olia Danilevich。
到此为止,应该可以清楚地看到,踏板板非常适合自然音频数据增强。然而,在实际使用案例中,还有一些额外的事项需要考虑。
“好”的预设
如果我们想保持增强效果自然,我们需要找到一个(或多个)预设,能够可靠地创建自然的增强效果,适用于我们数据集中的所有示例。例如,为重金属曲目添加更多失真可能是一个可行的选择,但将古典音乐中的小提琴失真处理真的自然吗?
增强多样性
当然,我们可以对所有我们想增强的示例使用相同的效果链。然而,对每个示例在效果链中引入一些随机性有两个显著的优点:
-
你可以创建相同示例的多个增强效果,它们会有所不同。
-
模型学习处理更多样化的效果,这可以提高其鲁棒性。
在保持增强自然性的同时使用随机效果链是这里的关键挑战。
速度
如果我们想处理成千上万的音频示例,我们需要考虑一个高效的实现方式。我们当然可以增强每个文件并将增强后的示例存储为音频文件。然而,我们真的有兴趣将增强效果存储在某个地方吗?如果没有,我们可以在数据处理脚本或管道中计算增强效果。例如,如果我们想计算每个音频示例的梅尔谱图,我们可以
-
将音频加载到 numpy 数组中。
-
随机生成一个踏板板并将其应用于数组。
-
计算原始音频和增强示例的梅尔谱图。
-
将两个梅尔谱图都存储在训练数据集中。
这样,不会浪费时间在导出和加载我们不会再使用的音频文件上。
踏板板增强 GitHub 仓库
截图由作者提供。
为了帮助你开始进行音频增强,我写了一些代码并将其发布在这个 GitHub 仓库中。该仓库包括
-
一个实现随机、自然效果链的音频增强模块。
-
示例脚本用于将这些随机、自然的增强应用于单个文件或作为梅尔谱图特征提取管道的一部分
-
从 GTZAN 数据集中获得了一些示例音频文件,你可以用来玩弄增强效果。
虽然我无法在这篇文章中详细讲解代码,但我仍然想展示如何将随机效果链应用于单个音频文件。有关进一步的指导,请查看仓库中的“README.md”文件。
构建随机效果链
首先,我们将音频增强模块(库中的“code/audio_augmentation.py”)的路径添加到 sys.path 中,并导入相关功能。
import sys
sys.path.append("../code/")
from audio_augmentation import roll_pedal, pedal_dict, process_track
接下来,我们通过指定正确顺序中希望使用的效果来定义效果链。此外,我们为每个踏板分配一个概率值,表示每次踏板板滚动时该踏板被激活的可能性。如果你希望每次都应用所有踏板,请将所有概率设置为 1。
pedal_config = [("compressor", .3),
("chorus", .3),
("reverb", .3),
("distortion", .3),
("lowpassfilter", .3),
("highpassfilter", .3),
("pitchshift", .3)
]
接下来,我们定义一个备用效果链。这在没有踏板通过随机滚动的情况下需要,即没有效果被选择。在这种情况下,模块将使用这个备用效果链作为默认设置。
from pedalboard import Compressor, PitchShift, HighpassFilter
fallback = [HighpassFilter(cutoff_frequency_hz=300),
PitchShift(semitones=1),
Compressor(threshold_db=-10, ratio=1.3)]
滚动踏板板
要“滚动”我们的踏板板,我们可以使用这段代码,它调用了增强模块中的“roll_pedal”函数:
pedal_rolls = [roll_pedal(pedal=pedal_dict[pedal]["pedal"],
param_dict=pedal_dict[pedal]["param_dict"],
pedal_prob=prob,
random_seed=None)
for pedal, prob in pedal_config]
请注意,“pedal_dict”是从增强模块导入的。它保存了参数范围的信息,并为每个踏板应用了随机设置。
应用随机踏板板
现在,最后,我们可以加载我们的音频文件,并使用增强模块中的“process_track”函数将随机踏板板应用于它。
audio, sr = librosa.load("path/to_file")
audio_processed = process_track(
audio, sample_rate=sr,
pedals=pedal_rolls, fallback=fallback
)
让我们听听这段代码会对我们的示例音轨做什么!
这个效果有轻微的合唱效果以及一些轻微的失真。它听起来明显不同于原始版本,但仍然非常接近。在此期间,让我们再尝试另一种随机增强滚动!
这里,我们对信号应用了压缩器、低通滤波器、高通滤波器和音高移位。这次差异更为明显。然而,音轨的自然性仍然得到保持。
结论
在这篇文章中,我们
-
学习了数据增强是什么以及为何使用它。
-
讨论了自然增强为何如此重要。
-
看到音频数据增强与其他数据类型的增强有所不同。
-
获得了将踏板板库应用于音频信号的实际经验。
-
概述了音频数据增强的最佳实践。
-
初步了解了一个基于踏板板的音频增强模块。
希望你学到了一些新知识,并觉得这篇文章有用!
如果你对其他形式的音频数据增强感兴趣,可以查看我写的 这篇文章,讲述了如何使用“分而治之”方法进行音乐类型分类。
自然语言基础——情感分析、机器翻译和命名实体识别的介绍与语言模型实现
自然语言领域的多语言建模
·发布在 Towards Data Science ·阅读时间 9 分钟·2023 年 3 月 20 日
–
图片由 RetroSupply 提供,来自 Unsplash
我们人类使用语言、声音、手势和符号,以不同的形式(如演讲、写作和标志)传达复杂的概念和抽象。随着计算机的出现,为了利用这些强大的机器,我们不得不想出让计算机理解人类交流和现有知识库的方法。因此,自然语言处理(NLP)、理解(NLU)和生成(NLG)这三个人工智能的分支应运而生。这三个领域的界限有时并不清晰,而整体自然语言空间涵盖了当今计算机和数据科学世界中的各种应用。最常见的应用包括(I)情感分析,(II)机器翻译和(III)命名实体识别(NER),我们将在本文中对这些应用进行定义和实现。
阅读 Farzad(以及 Medium 上其他作者)的每一个故事。您的会员费用直接支持 Farzad 和其他人……
medium.com](https://medium.com/@fmnobar/membership?source=post_page-----c79662e52624--------------------------------)
为了实现这三项任务,我们将利用现有的“预训练语言模型”。因此,让我们首先了解什么是语言建模。我建议浏览一下“语言建模”部分,但如果你主要对应用和/或实现感兴趣,可以跳过这部分。
语言建模
语言建模包括使用统计和概率的方法,结合机器学习架构(如特别是深度神经网络)来确定一串文字中词语序列出现的可能性,例如一个句子。基于计算出的概率,可以做出某些决策——例如,一个模型可以生成对用户提供的提示的字符串/响应(如 ChatGPT),执行文本分类以确定某个词是否为名词、动词等。由于如今我们周围有大量的文本数据,这些语言模型通常在大量文本数据上进行训练。因此,这些模型也被称为大语言模型。此时你可能会想这与我们的帖子有何关联——我们正要到达这里。这些预训练的语言模型可以进一步训练(即微调)以执行特定任务,如情感分析、机器翻译和命名实体识别,我们今天将详细探讨这些任务。深入了解语言模型的架构和训练策略超出了本文的意图,但如果你对这一主题感兴趣,可以访问以下帖子:
预训练模型基础及其下游任务。
既然我们对自然语言领域和语言建模有所了解,接下来就进入使用这些模型的有趣部分吧!
1. 情感分析
识别文本情感的任务,例如判断它是积极的、消极的还是中立的,称为情感分析。它被用于社交媒体监控、客户反馈分析和产品评论分析等应用中。可以想象,这对许多公司来说非常有用。例如,一家大型在线零售公司无法分配足够的人力资源来手动阅读关于各种产品的所有评论。相反,他们可以对评论运行情感分析模型,并分析结果,从而节省时间和成本。接下来,让我们看看如何实现这一点。
1.1. 情感分析 — 实现
在这个示例中,我们首先从 Transformers 库中加载一个预训练的语言模型。接着,我们使用模型从输入句子中生成情感分析。最后,我们在两个不同的句子上测试它,一个是积极的,另一个是消极的,以验证模型的性能。以下是我们将使用的两个句子:
-
I loved this movie!
,我们期望模型将其分类为“积极”情感 -
I did not like this movie.
,我们期望模型将其分类为“消极”情感
让我们看看它是如何工作的!
# Import libraries
from transformers import pipeline
# Load the pre-trained model
nlp = pipeline('sentiment-analysis', model='distilbert-base-uncased-finetuned-sst-2-english')
# Define the function to perform sentiment analysis
def sentiment_analyzer(input_text):
# Perform sentiment analysis
result = nlp(input_text)[0]
# Return results
return f"'{input_text}' has a {result['label']} sentiment, with a score of {round(result['score'], 4)}!\n"
# Define example sentences
sentence_1 = "I loved this movie!"
sentence_2 = "I did not like this movie."
sentence_list = [sentence_1, sentence_2]
# Analyze the sentiment of each sentence
for sentence in sentence_list:
print(sentiment_analyzer(sentence))
结果:
结果看起来非常好,如我们所期待的。如果你对情感分析有更深入的兴趣,以下帖子适合你:
情感分析——简介与实现
使用 NLTK、scikit-learn 和 TextBlob 进行情感分析
towardsdatascience.com
接下来,让我们讨论机器翻译。
2. 机器翻译
使用计算机将文本从一种语言翻译到另一种语言的任务称为机器翻译。对大多数用户来说,最著名的例子是 Google 翻译——Google 翻译所做的就是机器翻译!应用非常广泛。例如,人们可以阅读和理解其他语言的信息。
2.1. 机器翻译——实现
要实现机器翻译,我们将使用由Facebook AI开发的 mBART-50,它来自Transformers库,这是一个用于机器翻译的预训练语言模型。步骤与我们在情感分析中做的非常相似,具体如下:
-
按如下方式安装 Transformers:
pip install transformers
-
导入库
-
加载预训练模型
-
通过模型运行示例句子并返回结果
mBART-50 的有趣之处在于它是一个多语言机器翻译模型,这意味着它可以进行不同语言之间的翻译。让我们测试一下这个功能,将一个句子翻译成 6 种不同的语言,使用一个模型!
提示: 我强调了这个语言模型的多语言特性,因为历史上,神经机器翻译模型只能从一种特定语言翻译到另一种特定语言。例如,我们需要为从英语到法语的翻译训练一个特定的模型,并为从英语到德语的翻译训练另一个模型。另一方面,这些多语言机器翻译模型可以使用一个模型在多种语言之间进行翻译,这非常令人印象深刻!
在下面的代码块中,我们导入库,加载模型,然后创建 translator
函数,该函数接受三个参数:(1)一个句子(source_sentence
),(2)提供的句子的语言(source_language
)以及(3)我们希望翻译成的语言(target_language
)。然后 translator
按照指示返回翻译结果。
# Import library
from transformers import MBartForConditionalGeneration, MBart50TokenizerFast
# Load model and tokenizer
model = MBartForConditionalGeneration.from_pretrained("facebook/mbart-large-50-many-to-many-mmt")
tokenizer = MBart50TokenizerFast.from_pretrained("facebook/mbart-large-50-many-to-many-mmt")
def translator(source_sentence, source_language, target_language):
# Encode sentence
tokenizer.src_lang = source_language
input_ids = tokenizer(source_sentence, return_tensors="pt").input_ids
# Translate sentence
output_ids = model.generate(input_ids, forced_bos_token_id=tokenizer.lang_code_to_id[target_language])
translation = tokenizer.batch_decode(output_ids, skip_special_tokens=True)[0]
# return translation
return translation
接下来,让我们通过将 Multilingual machine translation is impressive!
翻译成法语、西班牙语、意大利语、德语、简体中文和日语来测试我们的函数。
# Define sentence to be translated
original_sentence = 'Multilingual machine translation is impressive!'
# Define source language
english = "en_XX"
# Define target languages
french = "fr_XX"
spanish = "es_XX"
italian = "it_IT"
german = "de_DE"
simplified_chinese = "zh_CN"
japanese = "ja_XX"
# Create a list of target languages
target_list = [french, spanish, italian, german, simplified_chinese, japanese]
# Create a prompt list of lists
prompt_list = []
for target in target_list:
prompt_list.append([original_sentence, english, target])
# Create translations
print(f"Generating machine translations for: \n'{original_sentence}'\n")
for i in enumerate(prompt_list):
translation = translator(source_sentence=i[1][0], source_language=i[1][1], target_language=i[1][2])
print(f"{i[1][2]}:")
print(f"{translation}\n")
结果:
我们可以在结果中看到目标语言的翻译!我个人不讲这些语言,但我使用 Google 翻译验证了这些翻译,结果似乎是准确的!
最后但同样重要的是,让我们看一下命名实体识别。
3. 命名实体识别(NER)
识别和分类文本中的命名实体,并将其分类到预定义类别中的任务称为命名实体识别,简称 NER。这些类别的一些示例包括:人名、地点、日期、组织、数字等。你可能会想,为什么我们需要这样的模型。NER 在自然语言领域有许多应用。例如,Visa、美国运通、亚马逊等可以使用 NER 识别和屏蔽客户通信中的敏感信息,以保护客户的敏感信息,如出生日期和信用卡信息。另一种应用是社交媒体公司(如 Meta)识别评论/帖子中的地点和个人姓名,并将其用于内容推荐。
现在我们了解了 NER 的基本概念,让我们实现它并查看结果。
3.1. NER — 实现
在这个例子中,我们将使用 spaCy 的预训练模型进行命名实体识别(NER)。实现过程非常简单。我们将按照以下步骤进行:
-
安装并下载所需的数据
-
导入库
-
加载预训练任务,包括 NER
-
通过模型运行一个示例句子并返回结果
如果你需要安装 spaCy 并下载数据,你可以使用以下命令 (source):
pip3 install spacy
python -m spacy download en_core_web_sm
我通过命令行界面完成了上述安装。操作步骤很简单:(I)打开终端,然后(II)运行上述两行命令。如果你需要更详细的命令行教程,请随时查看以下内容:
## 命令行界面(CLI)教程 — 高级用户如何与计算机互动
CLI 入门以提高与计算机互动的生产力
towardsdatascience.com
接下来,让我们对以下句子实施和应用命名实体识别(NER):Farzad wrote this Medium article in March 2023, using an Apple laptop, on a Jupyter notebook!
# Import library
import spacy
# Load English tokenizer, tagger, parser and NER
nlp = spacy.load("en_core_web_sm")
# Define example sentence
sentence = "Farzad wrote this Medium article in March 2023, using an Apple laptop, on a Jupyter notebook!"
# Apply NER
doc = nlp(sentence)
# Analyze syntax
print(f"Noun phrases:{[chunk.text for chunk in doc.noun_chunks]}\n")
print("Verbs:", [token.lemma_ for token in doc if token.pos_ == "VERB"])
print("")
# Find named entities, phrases and concepts
for entity in doc.ents:
print(entity.text, entity.label_)
结果:
这非常有趣!让我们来谈谈结果。第一行识别了名词——我个人不完全同意所有这些,但仍然,这非常令人印象深刻!第二行正确地识别了“write”和“use”作为动词,第三块识别了“Medium”作为地点,“March 2023”作为日期,“Apple”作为组织(这一点很有趣,因为 apple 也可能是水果的名称,但模型根据句子的上下文识别了公司名称)和“Jupyter”作为一个人(这一点需要一些改进)。有方法进一步训练这些预训练模型,以确保 NER 在每个使用场景中更准确,但我们想要表达的重点是展示这些预训练语言模型如何以合理的准确性完成复杂任务,如 NER。
结论
在这篇文章中,我们简要地介绍了自然语言处理(NLP)、理解(NLU)和生成(NLG)的世界,并通过引入和实施自然语言领域中的一些常见任务,使用语言建模来理解它们的重要性。然后,我们介绍了(I)情感分析,(II)机器翻译和(III)命名实体识别(NER)的语言模型实现,并查看了这些强大的预训练语言模型在多种语言中的令人印象深刻的结果。
感谢阅读!
如果你觉得这篇文章对你有帮助,请关注我在 Medium并订阅以接收我最新的文章!
(所有图片,除非另有说明,均由作者提供。)
自然语言处理初学者指南
原文:
towardsdatascience.com/natural-language-processing-for-absolute-beginners-a195549a3164
用 10 行 Python 代码解决复杂的 NLP 任务
·发表于 Towards Data Science ·阅读时间 9 分钟·2023 年 9 月 2 日
–
作者提供的图片(使用 Craiyon 生成)
NLP(自然语言处理)通常被认为是计算机科学中的复杂领域。像 SpaCy 或 NLTK 这样的框架体积庞大,通常需要一些学习。但借助开源的大型语言模型(LLMs)和现代 Python 库,许多任务可以更容易地解决。而且,甚至在几年前只有科学论文中才能找到的结果,现在也可以通过仅仅 10 行 Python 代码实现。
话不多说,我们开始吧。
1. 语言翻译
你是否曾想过 Google 翻译是如何工作的?Google 使用一个在大量文本上训练的深度学习模型。现在,借助 Transformers 库,不仅在 Google 实验室,还能在普通 PC 上完成这项工作。在此示例中,我将使用一个预训练的 T5-base(文本到文本转换器)模型。该模型最初在原始文本数据上训练,然后在诸如(“将英语翻译成德语:这所房子很棒”,“Das Haus ist Wunderbar”)的源-目标对上微调。在这里,“将英语翻译成德语”是一个前缀,告诉模型该做什么,而短语是模型应该学习的实际上下文。
重要警告。大型语言模型的体积确实很大。此示例中使用的 T5ForConditionalGeneration 类将自动下载约 900 MB 大小的“t5-base”模型。在运行代码之前,请确保有足够的磁盘空间,并且流量没有限制。
可以在 Python 中使用预训练的 T5 模型:
from transformers import T5Tokenizer, T5ForConditionalGeneration
preprocessed_text = "translate English to German: the weather is good"
tokenizer = T5Tokenizer.from_pretrained('t5-base',
max_length=64,
model_max_length=512,
legacy=False)
tokens = tokenizer.encode(preprocessed_text,
return_tensors="pt",
max_length=512,
truncation=True)
model = T5ForConditionalGeneration.from_pretrained('t5-base')
outputs = model.generate(tokens, min_length=4, max_length=32)
print("Result:", tokenizer.decode(outputs[0], skip_special_tokens=True))
#> Result: Das Wetter ist gut.
在这里,一个 T5Tokenizer 类将源字符串转换为数字形式;这个过程叫做 分词。在我们的例子中,一个“将英语翻译成德语:天气很好”的文本将被转换为 [13959, 1566, 12, 2968, 10, 8, 1969, 19, 207, 1] 数组。“生成”方法实际执行任务,最后,分词器进行反向转换。作为输出,我们将得到结果“Das Wetter ist gut”。
我们能否使这段代码更简洁?实际上,我们可以。借助 Transformer 的 pipeline 类,我们可以创建一个抽象管道,只需 2 行 Python 代码即可完成这个任务:
from transformers import pipeline
translator = pipeline("translation_en_to_de", model="t5-base")
print(translator("the weather is good"))
#> [{'translation_text': 'Das Wetter ist gut.'}]
出于自学的目的,我通常更喜欢第一种方法,因为它更容易理解“幕后”的情况。但对于“生产”目的,第二种方法更具灵活性,它还允许在不更改代码的情况下使用不同的模型。
2. 摘要生成
文本摘要的目标是将文档转换为简短版本,这显然如果手动完成会花费时间。令人惊讶的是,T5 模型也可以做到这一点;我们唯一需要更改的是前缀:
body = '''Obviously, the lunar surface is covered with craters, left
from previous collisions of meteorites with the Moon. Where does math go?
While a meteorite collision is a random event, its frequency
obeys probability theory laws. There is no atmosphere on the Moon's
surface, no erosion, and no wind. Therefore the lunar surface is an
ideal "book" in which the events of the last tens of thousands of
years are recorded. By studying the Moon, we can calculate how often
such objects fall on its surface.
A study of the lunar surface with high-resolution cameras is ongoing.
It has been estimated that at least 220 new craters have formed on the
Moon over the past 7 years. This check is also vital because
these calculations can help assess the danger to the Earth.'''
preprocessed_text = f"summarize: {body}"
tokenizer = T5Tokenizer.from_pretrained('t5-base',
max_length=256,
model_max_length=512,
legacy=False)
tokens = tokenizer.encode(preprocessed_text,
return_tensors="pt",
max_length=256,
truncation=True)
model = T5ForConditionalGeneration.from_pretrained('t5-base')
outputs = model.generate(tokens,
min_length=4,
max_length=64)
print("Result:", tokenizer.decode(outputs[0], skip_special_tokens=True))
#> the lunar surface is an ideal "book" in which the events of the last
#> tens of thousands of years are recorded. by studying the Moon, we can
#> calculate how often such objects are falling on its surface.
正如我们所见,结果非常准确。
与第一个例子一样,使用管道可以为相同的任务生成更简短的代码:
summarizer = pipeline("summarization", model="t5-base", tokenizer="t5-base")
summarizer(body, min_length=4, max_length=64)
#> [{'summary_text': 'the lunar surface is an ideal "book" in which the
#> events of the last tens of thousands of years are recorded . it has been
#> estimated that at least 220 new craters have formed on the Moon over the
#> past 7 years .'}]
读者可能对使用 “t5-base” 模型可以完成的其他任务感兴趣。我们可以轻松地将它们全部打印出来:
for prefix in model.config.task_specific_params:
print(f"{prefix}: {model.config.task_specific_params[prefix]}")
#> summarization:
#> {'early_stopping': True, 'length_penalty': 2.0, 'max_length': 200, 'min_length': 30, 'no_repeat_ngram_size': 3, 'num_beams': 4, 'prefix': 'summarize: '}
#> translation_en_to_de:
#> {'early_stopping': True, 'max_length': 300, 'num_beams': 4, 'prefix': 'translate English to German: '}
#> translation_en_to_fr:
#> {'early_stopping': True, 'max_length': 300, 'num_beams': 4, 'prefix': 'translate English to French: '}
#> translation_en_to_ro:
#> {'early_stopping': True, 'max_length': 300, 'num_beams': 4, 'prefix': 'translate English to Romanian: '}
3. 问答
大型语言模型还可以提供另一种有趣的功能,即在给定的上下文中回答问题。我将使用与之前示例相同的文本:
body = '''Obviously, the lunar surface is covered with craters, left
from previous collisions of meteorites with the Moon. Where does math go?
While a meteorite collision is a random event, its frequency
obeys probability theory laws. There is no atmosphere on the Moon's
surface, no erosion, and no wind. Therefore the lunar surface is an
ideal "book" in which the events of the last tens of thousands of
years are recorded. By studying the Moon, we can calculate how often
such objects fall on its surface.
A study of the lunar surface with high-resolution cameras is ongoing.
It has been estimated that at least 220 new craters have formed on the
Moon over the past 7 years. This check is also vital because
these calculations can help assess the danger to the Earth.'''
question_answerer = pipeline("question-answering",
model='distilbert-base-cased-distilled-squad')
result = question_answerer(question="Which surface has collision with meteorites",
context=body)
print(f"Answer: '{result['answer']}', score: {round(result['score'], 4)}, start: {result['start']}, end: {result['end']}")
#> Answer: 'the Moon', score: 0.4401, start: 93, end: 101
result = question_answerer(question="How many craters were formed",
context=body)
print(f"Answer: '{result['answer']}', score: {round(result['score'], 4)}, start: {result['start']}, end: {result['end']}")
#> Answer: 'at least 220', score: 0.5302, start: 600, end: 612
result = question_answerer(question="Is there atmosphere on the moon",
context=body)
print(f"Answer: '{result['answer']}', score: {round(result['score'], 4)}, start: {result['start']}, end: {result['end']}")
#> Answer: 'There is no', score: 0.2468, start: 220, end: 231
在这种情况下,我使用了一个 distilbert-base-cased-distilled-squad 模型,模型大小为 261 MB。正如我们所见,该模型不仅提供了答案,还从原始文本中检索了位置,这对验证结果很有帮助。
4. 语言生成
另一个有趣的过程是语言生成。对于这个例子,我将使用一个 GPT-2 模型。这显然不是我们今天拥有的最新 GPT 模型,但 GPT-2 是免费提供的,并且其文件大小为 548 MB,足够在普通 PC 上运行。
让我们看看它是如何工作的:
from transformers import GPT2Config, GPT2Model,
TFGPT2LMHeadModel, GPT2Tokenizer
tokenizer = GPT2Tokenizer.from_pretrained("gpt2")
input_ids = tokenizer.encode("I am going to say",
return_tensors='tf')
model = TFGPT2LMHeadModel.from_pretrained("gpt2",
pad_token_id=tokenizer.eos_token_id)
output = model.generate(input_ids,
max_length=128,
early_stopping=True,
do_sample=True,
top_k=20)
print("Output:", tokenizer.decode(output[0], skip_special_tokens=True))
#> Output: I am going to say something. It is very hard for people to
#> believe me because I do not have to speak English. But the fact that
#> you are writing a book on it is one thing. You have to have this
#> knowledge which is not from what you read; it is from an educated...
同样,可以使用更简短的代码通过管道完成:
from transformers import pipeline
generator = pipeline('text-generation', model='gpt2')
generator("I am going to say", max_length=128, num_return_sequences=1)
#> I am going to say this: I have had several bad experiences with the
#> internet in the years since my first connection. While there ...
实际上,我在这些文本中看不到太多意义,但从语法角度来看,它们足够好,对于某种自动化或单元测试,它们可能是有用的。有趣的是,GPT-2 模型于 2019 年发布。为了好玩,我向 GPT-3.5(发布于 2022 年)提出了相同的问题,“请继续我将要说的短语”,并得到了以下答案:
Am going to say that communication is a vital skill in today's interconnected
world. Whether it's expressing your thoughts and ideas, building relationships,
or resolving conflicts, effective communication plays a central role in almost
every aspect of our lives. It's not just about the words we use but also our
tone, body language, and the context in which we communicate.
这个结果要好得多;这三年取得了巨大的进步。但显然,即使 GPT-3.5 模型在今天发布于公共领域,也不可能在普通电脑上运行。
5. 情感分析
之前的示例主要是为了娱乐,但情感分析对于商业来说更为重要。情感分析是分析给定文本中的情感并找出主观意见的过程。对于网络商店、流媒体平台和其他许多用户可以发布评论的服务,这可能特别重要。
对于这个测试,我将使用一个distilbert-base-uncased-finetuned-sst-2-english模型:
from transformers import pipeline
sentiment_pipeline = pipeline("sentiment-analysis",
model='distilbert-base-uncased-finetuned-sst-2-english')
data = ["It was not bad",
"I expected to love it but I was wrong"]
sentiment_pipeline(data)
#> [{'label': 'POSITIVE', 'score': 0.9995607733726501},
#> {'label': 'NEGATIVE', 'score': 0.997614860534668}]
我故意尝试使用不易分析的短语,但模型给出了正确的答案。显然,自然语言是非常灵活的,仍然可能出现导致错误结果的文本。例如,这个模型对短语“我期待它很糟糕,但我错了”给出了错误的回答。同时,(体积大得多的)GPT-3.5 模型能够正确解析。另一方面,考虑到 DistilBERT 模型的大小仅为 268 MB 且可以免费使用(模型拥有 Apache 2.0 许可证),结果还是相当不错的。读者也可以尝试其他开源模型,并选择最适合他们需求的模型。
6. 命名实体识别(NER)
自然语言处理的另一个有趣部分是“命名实体识别”。这是从非结构化文本中提取实体,如名称、地点、日期等的过程。对于这个测试,我将使用一个bert-base-NER 模型(文件大小为 433 MB)。
让我们考虑一个例子:
from transformers import pipeline
body = "Hi, my name is Dmitrii. I am in London, I work in Super Company, " \
"I have a question about the hotel reservation."
ner = pipeline("ner",
model="dslim/bert-base-NER",
aggregation_strategy='average')
ner(body)
#> [{'entity_group': 'PER',
#> 'score': 0.7563127,
#> 'word': 'Dmitrii',
#> 'start': 15,
#> 'end': 22},
#> {'entity_group': 'LOC',
#> 'score': 0.99956125,
#> 'word': 'London',
#> 'start': 32,
#> 'end': 38},
#> {'entity_group': 'ORG',
#> 'score': 0.99759734,
#> 'word': 'Super Company',
#> 'start': 50,
#> 'end': 63}]
正如我们所见,该模型能够正确确定文本中提到的主要实体,如名称、地点和公司。
7. 关键词提取
在最后一个例子中,我们测试了 NER,但不是所有的参数都从文本中提取出来了。一个单独的关键词提取算法对于同一任务也可能很有用。对于这个例子,我将使用KeyBERT:
from keybert import KeyBERT
body = "Hi, my name is Dmitrii. I am in London, I work in Super Company, " \
"I have a question about the hotel reservation."
kw_model = KeyBERT()
keywords = kw_model.extract_keywords(body,
keyphrase_ngram_range=(1, 1),
diversity=0.8,
stop_words=None)
print(keywords)
#> [('reservation', 0.5935), ('hotel', 0.5729), ('london', 0.2705),
#> ('dmitrii', 0.2), ('company', 0.1817)]
正如我们所见,关键词提取作为 NER 的补充也很有用,它可以从相同的短语中提取一些额外的数据。
结论
在这篇文章中,我测试了不同的自然语言处理(NLP)算法。正如我在文章开头承诺的那样,通过现代库的帮助,复杂的任务可以在 10 行 Python 代码中解决。还要提到的是,这些代码都可以在本地运行,无需任何 API 订阅或密钥。最后但同样重要的是,我希望读者能看到 NLP 也可以很有趣。
感谢阅读。如果你喜欢这个故事,可以随时订阅Medium,你将会在我的新文章发布时收到通知,并且可以全面访问其他作者的成千上万篇故事。
自然语言处理不仅仅是聊天机器人
·
关注 发表在 Towards Data Science · 以 新闻通讯 形式发送 · 3 分钟阅读 · 2023 年 3 月 2 日
–
在我们集体注意力仍然集中在聊天机器人上时,很容易忘记自然语言处理(NLP)领域的广阔和多样性。从翻译到文本分类及其他,数据科学家和机器学习工程师们正在进行(并且仍在进行)一些令人兴奋的项目,这些项目的名称既不以 C 开头,也不以 T 结尾。
为了让这些子领域重新引起关注,我们很高兴分享我们近期的一些 NLP 收藏。它们将吸引任何喜欢处理文本数据的人——也包括那些对学习和实验感兴趣的数据从业者。
-
深入挖掘 NLP 项目的关键要素。Erwin van Crasbeek的详尽讲解是初学者和资深专家的完美起点。它涵盖了自然语言处理的历史和基本概念,然后解释了 Erwin 的荷兰语问答机器学习模型的内部工作原理。
-
语言检测是如何工作的? 如果你曾经使用过在线翻译工具,你可能体验过那种神奇的瞬间,工具识别了你输入文本的语言。Katherine Munro 将 Python、NLTK(自然语言工具包)平台和一点统计学结合在一起,揭示了这一魔法背后的过程。
图片由 Ross Sneddon 在 Unsplash 提供
-
不要被文本分类器的复杂性吓到。学习如何构建文本分类器“可能会有点棘手,”Lucy Dickinson 说。这正是 Lucy 的 10 步指南如此有用的原因:它将一个潜在的复杂过程分解为明确和清晰的任务(以及你开始自己动手所需的所有代码)。
-
为特定 NLP 任务找到合适数据的艺术。在大多数机器学习管道中,成功与失败的关键在于你使用的数据质量。Benjamin Marie 指出,对于机器翻译项目,我们不仅要考虑质量,还要考虑适用性;此外,真正知道如何从现有数据中挤出尽可能多的价值也很有帮助。
二月飞逝得如此之快;这里有一些我们还没来得及突出显示的亮点,但它们值得你花时间了解,因为我们正轻轻迈入三月。
-
如果你既不是深度学习新手也不是专家,千万不要错过Leonie Monigatti的新中级模型微调指南。
-
对于语言模型局限性的清晰探讨,尤其是它们取代搜索引擎的潜力,我们热情推荐Noble Ackerson的首篇 TDS 文章。
-
想要深入了解去噪扩散模型的工作原理?Wei Yi的新深度探讨是你所需的完整资源。
-
如果你刚开始处理地理空间数据,Eugenia Anello的最新贡献是 GIS 实用入门(以及关键术语和概念的实用指南)。
-
是否厌倦了一遍遍使用相同的软件包?Miriam Santos邀请你在可视化和 EDA 工作流中尝试五个开源 Python 包。
感谢你本周的时间和支持!如果你喜欢我们发布的工作(并且想要访问所有内容),请考虑成为 Medium 会员。
直到下一个 Variable,
TDS 编辑
行业特定 AI 的导航:从过渡性英雄到长期解决方案
该图像由 Midjourney 支持制作。
策略、洞察与行业特定大语言模型的演变
·发表于数据科学前沿 ·7 分钟阅读·2023 年 9 月 4 日
–
随着人工智能(AI)领域的不断发展,我们正目睹一个日益增长的趋势:专为特定行业量身定制的大语言模型(LLMs)的兴起。这些行业特定的 LLMs 不仅适应了特定领域的专业术语和背景,还提供了定制的 AI 解决方案,以应对该行业内的独特挑战。例如,在医疗领域,一个专业的 LLM 可以加速药物研究与发现,而在金融领域,相应的模型可以迅速解码复杂的投资策略。
在这种背景下,所谓的“行业大模型”可以基本理解为“应用于特定行业的一般大模型的扩展”。这里有两个核心概念需要强调:第一个是“一般大模型”,第二个是“行业特定数据”。
一般大模型的真正价值不仅在于其庞大的参数数量,更在于其在多个领域的广泛适用性。这种跨领域的普遍性不仅增强了模型的适应性,还在模型向更“通用”的方向发展时产生了独特的能力。因此,单纯使用行业特定数据来训练模型是一种短视的方法,这与一般大模型的核心理念“普遍性”基本相悖。
对于行业特定的数据,主要有两种应用方式。第一种是直接使用这些数据对一般的大型模型进行微调或继续训练。第二种方法利用提示或外部数据库,利用一般大型模型的“上下文学习”能力来解决特定行业问题。这两种方法各有优缺点,但它们都有一个共同的目标,即利用一般大型模型的能力,更准确地解决行业特定的挑战。
平衡即时利益与长期愿景
一般的大型语言模型在两个方面表现出显著的优势:“知识”和“能力”,其中知识可以类比为经验,能力则类似于智商(IQ)。
能力如智商: 在经过大量数据训练后,一般的大型模型表现得像智商极高的实体,具备迅速理解、学习和解决问题的能力。以数学问题解决为例:大型模型只需简要了解数学公式和概念,就能轻松解决复杂问题,这种情况被称为“零样本学习”。在提供几个示例问题的情况下,它甚至可以应对更复杂的数学挑战,这被称为“少样本学习”。相比之下,行业特定的大型模型深入研究各种专业书籍和问题集。当两个大型模型之间存在明显的智商差异时,例如 GPT-4 和 GPT-3.5,智商较高的模型即使经验较少,也会有更高的准确率。
知识如经验: 一般大型模型的知识来源于它们接触过的数据。这意味着,对于某些特定问题或领域,如果模型没有接触过相关数据,它就无法提供准确的答案。例如,当询问“费马最后定理”的证明时,除非模型接触过相关的学术文献,否则它无法准确阐述定理的证明。
在这种背景下,当通用大型模型在特定领域的能力不足时,行业特定的大型模型可以被视为“过渡性产品”。例如,在法律领域,ChatLaw 模型在法律问题和信息检索任务中表现出色,因为它经过大量专业法律数据的训练。然而,随着通用大型模型能力的持续提升,对这种专业训练的需求可能会减少。从长远来看,通过整合外部知识库或上下文信息,通用大型模型具有运用其高级推理能力来应对复杂行业挑战的潜力,逐渐取代行业特定的大型模型。这一趋势不仅在通用人工智能(AGI)的发展中不可避免,也代表了解决行业挑战的更根本的方法。
行业模型的策略
使用大型模型解决行业挑战可以大致分为三种主要方法:
基于混合数据从零开始构建
这种方法涉及使用通用数据和领域特定数据的组合,从零开始创建一个新的模型。显著的例子包括 BloombergGPT。然而,需要注意的是,这种方法可能需要大量的数据和计算资源。
调整通用模型的权重
这一类别包括两种策略:
-
在通用模型上继续预训练: 像 LawGPT 这样的模型就是这种策略的例子,通过继续预训练建立在通用模型的基础上。这一迭代过程增强了模型对特定领域的适应性。
-
指令调优(SFT): 这种方法在通用模型框架的基础上利用指令调优(SFT)。通过微调通用模型的权重,它可以更好地解决特定行业的问题。
这两种方法都旨在通过调整权重来适应行业特定需求。虽然这些方法通常比完全重训练消耗的资源少,但可能无法始终达到最佳性能水平。此外,与重训练行业特定大型模型相比,实现显著性能提升可能具有挑战性。
结合通用模型与领域知识
这一类别包括两种策略:
-
利用外部领域知识: 这种方法涉及将领域特定知识(例如利用向量数据库)增强通用大型模型。通过整合领域知识数据库中的信息,可以提高通用模型在应对行业特定问题时的效果。
-
利用上下文学习进行领域响应: 通过“上下文学习”,通过创建具有上下文相关的提示来解决特定行业的挑战。然后,通用的大型模型直接生成响应。随着对更广泛上下文窗口的趋势日益增强,提示可以涵盖更多领域特定的知识。因此,即使使用通用的大型模型,也可以有效地回应行业特定的问题。
总结来说,所有这些方法都有一个共同的目标,即解决行业特定的挑战,但它们在方法和资源需求上有所不同。选择合适的方法应考虑可用数据、计算能力和预期的性能提升。在评估行业特定的大型模型时,必须区分这些不同的方法,防止对工作负荷和成本不同的方法进行误导性的比较。
实际实施指南
在行业特定的大型模型领域,一个关键但经常被忽视的问题是数据比例。具体而言,实践者经常发现,使用大量行业特定数据进行模型微调可能会悖论地降低模型的能力。一种可能的解释是行业特定数据与一般数据之间的质量差异。此外,数据比例——专门数据与一般数据的混合比例——可能并不理想。例如,一些模型如 BloombergGPT 采用简单的 1:1 数据比例,但这种方法在预训练、持续预训练或微调中的有效性仍然是一个悬而未决的问题。
我在跨越不同领域(从电子商务和办公室管理到智能家居技术)开发领域特定语言模型的经验为我提供了宝贵的见解。通常,这些模型的规模为 13B,利用 20M 到 100M 的领域特定数据。我发现,在持续预训练阶段,约 10%到 20%的领域特定数据与总数据集的比例通常能提供最佳结果。超过这个比例往往会妨碍模型的通用能力,如总结和问答。
另一个有趣的观察是,在持续预训练过程中,同时融入用于监督微调(SFT)的数据是有益的。这使得模型即使在预训练阶段也能获得更多领域特定的知识。
但是,值得注意的是,这个 10%到 20%的比例并不是一种通用的解决方案。它可能会根据所使用的特定预训练模型以及模型大小和原始数据比例等其他因素而有所不同。调整这些比例通常需要通过实证调整,以实现通用能力和领域特定能力之间的理想平衡。
对于监督微调(SFT),数据比例可以更加灵活,有时甚至可以达到 1:1 的平衡以获得良好的结果。但这种灵活性取决于 SFT 中使用的数据总量。如果数据集较小,这种混合数据方法的影响可能会很小。
在制定应对行业特定挑战的策略时,必须根据任务复杂性量身定制方法。对于较简单的任务,精心制作的提示通常更有效,特别是当与向量数据库结合使用时。这种方法可以解决大多数行业问题。对于中等复杂度的任务,监督微调(SFT)通常更有效,尤其是当混入一些通用数据时。这可以解决大多数剩余问题。对于更复杂的挑战,可能需要等待通用大模型进一步提升其能力。
结论
随着人工智能领域的不断拓展,行业特定的大模型在满足专业需求方面变得越来越重要。然而,必须理解这些专业化模型不是独立存在的构件。它们本质上是针对特定行业需求定制的通用大模型的专门应用。这些行业特定的迭代版本充当了过渡性解决方案,弥补了当前通用模型无法填补的空白。然而,随着通用模型的持续演进,这种专业化适配的需求可能会减少,更多的通用解决方案可能会取而代之。
选择适当的策略来应对行业特定的挑战是一个复杂的过程。其中的复杂性包括常被低估的数据比例因素,这可以深刻影响模型的有效性。对于简单的任务,使用精心设计的提示可以产生优异的结果,而对于更复杂的问题,可能需要不断发展的通用大模型的增强能力。在这个不断变化的环境中,开发有影响力的行业解决方案的关键在于对数据类型、模型架构和任务复杂度的精细平衡。
导航制图挑战:#30 天地图挑战中途进行中
·
关注 发表在Towards Data Science ·7 min read·Nov 17, 2023
–
作者提供的图片
每张地图都讲述一个故事,每个故事都让我们更接近理解我们的世界。
视觉化世界的广阔是一项了不起的壮举。但为了更接近它,今年十一月,我参加了#30 天地图挑战。我一直迷恋远处的地理可视化,并自己尝试过一些,但我想更深入地学习如何制作能讲述故事的美丽地图。因此,我参加了#30 天地图挑战。
什么是#30 天地图挑战?
#30DayMapChallenge 是一个由社区驱动的活动,每年 11 月举行。这个活动的想法是围绕不同的每日主题创建地图,并使用标签#30DayMapChallenge。对于制作地图所用的工具、技术或数据没有限制。
这个挑战为我提供了一个完美的机会,让我深入探索制图的世界(双关语)。我们已经进入了十一月中旬,我最喜欢的部分是从社区中学习并受到启发,同时每天挑战自己的创造力。
在这篇文章中,我将分享一些我至今最喜欢的地理可视化图,这些图是我使用Observable Plot这一 JavaScript 库为探索性数据可视化而创作的。你可以在我的#30DayMapChallenge集合中找到所有可视化图以及数据源和代码。我选择使用 Observable Plot,因为我在参加今年早些时候的#30DayChartChallenge时对其简洁性和易用性爱不释手。
本文中的所有图片均由作者创作。
第 1 天 — 点位
第 1 天 — 点位
十月已经过去,但灵异事件仍在继续。对于这个主题,我选择了在美国绘制鬼魂地点。我从数据世界网站获取数据,这是我最喜欢的数据集发现来源之一。这个数据集包括了每个报告的鬼魂地点的纬度和经度,非常适合绘制成“点”。
我使用了 Albers 投影来绘制美国地图,并给它设置了黑色背景以形成鲜明的对比效果。这个图的主要目标不仅是绘制每个位置的点位,还要创造一种发光效果,以契合我的“鬼魂”主题。为此,我将每个报告的地点用三个点层叠在一起。首先是一个大“红色”点,透明度最低;然后是一个“橙色”点,半径较小且透明度较高;最后是一个“白色”点,半径最小且透明度最高。这些层叠的点位产生了一个带有火焰光环和发光效果的白色点。我不认为这种组合在每个图上都会产生类似的效果,但所有点位的紧密度创造了我想要达到的完美效果。
这个图确实把许多东海岸的地点加入了我未来旅行的清单,也许在这一生中,或者也可能作为一个幽灵。
第 7 天 — 导航
第 7 天 — 导航
我最喜欢的导航方式是 Amtrak 系统,因此在导航主题中,我决定绘制加州的 Amtrak 车站,它是车站最多的州,数据来源于data.world。
在可视化加州的 Amtrak 车站网络时,我旨在突出铁路旅行在金州内的连通性和覆盖范围。我选择了墨卡托投影以准确表示州级网络,背景使用从太平洋到西南的颜色渐变。每个车站都用一个‘点’标记,主要枢纽用更大、更显眼的标记区分。为了进一步展示铁路系统的扩展和覆盖范围,我叠加了 Voronoi 网络图。这张地图不仅作为潜在旅行者的工具,还展示了支持在这个广阔州内移动的基础设施。
第 8 天 — 非洲
第 8 天 — 非洲
处理非洲的主题时,我聚焦于乌干达的水源数据,数据来源于水点数据交换通过TidyTuesday。
数据提供了一个全面的水点列表,我使用墨卡托投影绘制了这些水点,以准确表示它们的分布。地图的深色背景使水源点突出,吸引注意力到水源密度较高的区域。我将这些位置绘制为栅格地图,以添加纹理层。这张地图不仅展示位置;它讲述了获取水的故事,点的聚集可以指示潜在的水资源压力或丰富区域。
第 10 天 — 北美
第 10 天 — 北美
美国众议院选举结果地图是对美国五十多年来政治潮流的紧凑可视化。我从麻省理工学院选举数据与科学实验室通过TidyTuesday获取了数据,提供了按州详细的投票统计信息。
挑战在于以一种既有信息性又引人入胜的方式展示这个庞大的数据集。我选择了网格布局,每个州的投票趋势通过迷你条形图表示,允许立即在年份和州之间进行视觉比较。颜色编码非常简单——蓝色代表民主党,红色代表共和党,灰色代表其他——以便一眼就能清晰地洞察政治格局。这个地图作为美国政治的视觉历史,展示了可能无法仅从数字中看出的变化和模式。
第 12 天 — 南美
第 12 天 — 南美
对于这个主题,我找到了一组强大的数据集,来自于movebank.org,通过data.world关于南美土耳其秃鹫迁徙的数据。
这幅数据可视化图旨在捕捉鸟类移动的动态特性。数据包括标记秃鹫的时间戳位置,使我能够绘制它们随时间的旅程。我选择了暗色背景来象征覆盖的广阔区域,迁徙路径则用颜色渐变突出,便于眼睛跟踪模式。这幅地图并非静态的;它设计为代表迁徙的涨落,更集中的点可能显示重要的休息地。它提供了自然模式的一瞥,并作为保护主义者追踪这些迁徙路线健康状况的工具。
这绝对是我创建的最喜爱的动画可视化之一,因为它如此轻松地促进了清晰的迁徙模式的观察。这些数据还有很多可以做的,而我感觉自己只是刚刚触及表面。
第 13 天 — 分级地图
第 13 天 — 分级地图
2020 年非洲政治权利的区域分级地图,深刻描绘了这个大陆多样的政治景观。使用来自Freedom House通过TidyTuesday的数据,我旨在展示大陆各地政治权利不同层次的差异。
这幅地图使用绿色调来表示政治权利的渐变,较深的色调表示更大的自由。这种颜色选择是象征性的,在许多文化中,绿色被视为‘继续前进’,表明政治权利在进步的地区。除了其美学价值外,这幅地图还作为分析工具,呈现了复杂且不断发展的政治气候的快照。
中途反思
当我们达到#30daymapchallenge 的中点时,这正是停下来反思迄今为止旅程的完美时刻。每天制作地图,而不是图表,是一种不同类型的探索 —— 将数据和地理,故事和符号融合在一起。
在继续挑战的下半程中,我很兴奋地深入探索地图制作艺术,运用迄今为止学到的教训。对未来的挑战者们:让每个主题激发灵感,向社区学习,并将每一幅地图视为数据可视化旅程中的一步。
完成整个挑战后,我期待分享更丰富的可视化内容及其给予我的洞察。在此之前,我鼓励读者深入挖掘这些数据集,创造并分享他们自己的解读,以及参与这一全球地图挑战。
您可以在我的Observable collection中找到本文中所有可视化的代码和数据复现。
如果您愿意,可以在Linkedin上找到我。
探索聚类领域
原文:
towardsdatascience.com/navigating-the-clustering-landscape-b7930ac44147
聚类 | 机器学习 | Python
主要聚类算法的比较及实用 Python 示例
·发表于 Towards Data Science ·阅读时间 7 分钟·2023 年 5 月 29 日
–
照片来源:Aleks Dorohovich 在 Unsplash
机器学习领域不断发展,近年来引起了极大的兴趣。尤其是在当前 AI 热潮的影响下,数据科学和机器学习的神秘世界无疑受益于广泛的公众曝光。
在本文中,我们将把焦点放在机器学习的一个迷人子领域——聚类上。
让我们尝试描绘一个图景,好吗?
假设你正站在一个繁忙的城市中,周围是成千上万的人。在那一刻,每个人都是 100%独特的。他们是各自独立的个体——然而,你们都有共同的特征或属性,将你们作为一个群体连接在一起。也许是你们的时尚感、职业、政治或宗教信仰,甚至是你们喜欢的食物。
这就是聚类的本质——试图在大量数据点中发现隐藏的模式和分组。在机器学习的世界里,聚类技术是数据宇宙的制图师,描绘出原本看似随机或脱节的信息星座。
在本文中,我们将深入探讨主要的聚类技术类型,揭示它们的独特特性、优势以及应用。不论你是经验丰富的数据专业人士还是好奇的读者,我邀请你加入我这次激动人心的探索之旅。
K 均值聚类
让我们假设一下,我们是养蜂人。我们有一群蜜蜂在嗡嗡作响,我们的目标是将它们分成 K 个不同的蜂巢(即我们的聚类)。
首先,我们随机选择 K 只蜜蜂。这些蜜蜂将作为我们的簇质心。
就像蜜蜂被吸引到蜜糖一样,每只蜜蜂(数据点)都会向最近的蜂巢(质心)靠拢。
当所有的蜜蜂都找到一个蜂巢后,我们将确定每个蜂巢的新质心(更新质心)。我们会不断重复这个过程,直到蜜蜂安定下来,不再交换蜂巢。
啪嗒,那就是 K-means 聚类的本质!
K-means 聚类流程图。图片由作者提供。
优点
-
简单易懂
-
易于扩展
-
在计算成本方面效率高
缺点
-
需要提前指定 K
-
对初始质心选择敏感
-
假设簇是球形且大小相等(这可能并不总是如此)
完美的应用场景
-
市场细分
-
文档聚类
-
图像分割
-
异常检测
Python 示例
from sklearn.cluster import KMeans
import numpy as np
# Let's assume we have some data
X = np.array([[1, 2], [1, 4], [1, 0], [4, 2], [4, 4], [4, 0]])
# We initialize KMeans with the number of clusters we want
kmeans = KMeans(n_clusters=2, random_state=0).fit(X)
# We can get the labels of the data points
print(kmeans.labels_)
# And we can predict the clusters for new data points
print(kmeans.predict([[0, 0], [4, 4]]))
# The cluster centers (the mean of all the points in that cluster) can be accessed with
print(kmeans.cluster_centers_)
层次聚类(凝聚型聚类)
假设你正在参加一个大家庭的婚礼,亲属关系不明确。
你的第一个任务是识别直系亲属,如兄弟姐妹或父母和子女,并将他们聚在一起。
接下来,你寻找与这些已建立的群体有密切关系的其他亲属,并将他们纳入其中。
你继续这个过程,逐渐拼凑出整个家庭和朋友的网络,直到每个人都互相连接。
啪嗒,那就是层次聚类的本质!
层次聚类流程图。图片由作者提供。
优点
-
无需指定簇的数量
-
提供一个簇的层次结构,这可能是有用的
缺点
-
对大型数据集计算成本高
-
对距离度量的选择敏感
完美的应用场景
-
基因测序
-
社会网络分析
-
构建分类树
Python 示例
from sklearn.cluster import AgglomerativeClustering
import numpy as np
# Let's assume we have some data
X = np.array([[1, 2], [1, 4], [1, 0], [4, 2], [4, 4], [4, 0]])
# We initialize AgglomerativeClustering with the number of clusters we want
clustering = AgglomerativeClustering(n_clusters=2).fit(X)
# We can get the labels of the data points
print(clustering.labels_)
基于密度的空间聚类算法(DBSCAN)
想象一下你是一个狂热的鸟类观察者。
拿着你可靠的双筒望远镜,你四处扫描地平线,寻找空中鸟群。每当你发现一只鸟时,你会环顾四周,看看是否在给定半径内(数学上定义为 epsilon)有特定数量的鸟(我们称这个概念为最小点数或 MinPts)。
如果你的观察结果验证了,恭喜!你已识别出一个鸟群!
接下来,你寻找更多在这个簇范围内的鸟,并将它们纳入群体。这个模式会持续,直到所有的鸟都被标记为群体的一部分或被确定为孤立飞行者(异常值)。
啪嗒,那就是 DBSCAN 的本质!
DBSCAN 聚类流程图。图片由作者提供。
优点
-
无需指定簇的数量。
-
可以发现任意形状的簇。
-
擅长将高密度簇与低密度噪声分开。
缺点
-
不适合具有不同密度簇的数据。
-
对 epsilon 和 MinPts 的选择敏感。
完美的用例
-
空间数据分析
-
异常检测
-
图像分割。
Python 示例
from sklearn.cluster import DBSCAN
import numpy as np
# Let's assume we have some data
X = np.array([[1, 2], [2, 2], [2, 3], [8, 7], [8, 8], [25, 80]])
# We initialize DBSCAN with the epsilon and minPts we want
clustering = DBSCAN(eps=3, min_samples=2).fit(X)
# We can get the labels of the data points
print(clustering.labels_)
均值漂移聚类
想象一下你身处一个漆黑的房间,只能用手电筒,你的目标是找到你的朋友们。
一旦你打开手电筒,它会照亮附近的一些人。然后你评估他们的平均(均值)位置,并将手电筒的光束调整到那个方向。
你继续重复这个过程,每次都将光调整到计算出的均值位置,直到你发现自己在朋友们中间。
而这就是均值漂移聚类的精髓!
均值漂移聚类流程图。图片由作者提供。
优点
-
无需指定簇的数量
-
可以发现任意形状的簇
缺点
-
对大数据集来说计算开销较大。
-
带宽参数可以极大地影响结果。
完美的用例
-
图像分割
-
视频追踪
-
计算机视觉
Python 示例
from sklearn.cluster import MeanShift
import numpy as np
# Let's assume we have some data
X = np.array([[1, 1], [2, 1], [1, 0], [4, 7], [3, 5], [3, 6]])
# We initialize MeanShift with the bandwidth we want
clustering = MeanShift(bandwidth=2).fit(X)
# We can get the labels of the data points
print(clustering.labels_)
# The cluster centers can be accessed with
print(clustering.cluster_centers_)
谱聚类
想象你是一个音乐指挥,任务是从一群音乐家中组建一个乐团。
你首先听每位音乐家的表演。你记录他们音乐风格的相似性,创建一种“音乐兼容性图表”——你的相似性矩阵。
这个图表作为模板(和谐蓝图——使用拉普拉斯方法计算),你可以根据它来识别出相似的不同簇。从数学上讲,这是通过特征值分解实现的。
最终,每位音乐家根据这种分析(使用 K 均值聚类)找到自己在特定组中的位置。
而这就是谱聚类的精髓!
谱聚类流程图。图片由作者提供。
优点
-
可以发现任意形状的簇。
-
对于小数量的簇效果良好。
缺点
-
对大数据集来说计算开销较大。
-
相似性度量和簇的数量选择会极大地影响结果。
完美的用例
-
图像分割
-
社交网络分析
-
基因表达分析
Python 示例
from sklearn.cluster import SpectralClustering
import numpy as np
# Let's assume we have some data
X = np.array([[1, 1], [2, 1], [1, 0], [4, 7], [3, 5], [3, 6]])
# We initialize SpectralClustering with the number of clusters we want
clustering = SpectralClustering(n_clusters=2, assign_labels="discretize", random_state=0).fit(X)
# We can get the labels of the data points
print(clustering.labels_)
结论
在这篇文章中,我们深入探讨了一些在机器学习领域至关重要的聚类算法。我们审视了这些方法如何实现目标,讨论了它们的优缺点,并强调了它们最有效的场景。
选择合适的聚类技术就像选择适当的工具来完成特定任务一样。重要的是要考虑你处理的数据类型和你要解决的具体问题。例如,处理层次数据时,使用基于密度的聚类方法是不明智的。
机器学习中的聚类领域是一个充满活力的景观,充满了探索、发现和突破性想法的可能性。
当我们持续深入探索和掌握这一领域时,我们不仅仅是在使数据更易理解;我们还在为开创性的进步奠定基础,这些进步可能会重塑我们与周围世界的互动方式。因此,请保持探索的精神,继续丰富你的理解,永远不要停止聚类!
你喜欢这篇文章吗?每月$5,你可以成为会员,解锁对 Medium 的无限访问权限。这将直接支持我和你在 Medium 上喜欢的其他作者。非常感谢!
## 通过我的推荐链接加入 Medium - David Farrugia
获得我所有⚡优质⚡内容和 Medium 上的无限访问权限。通过请我喝一杯咖啡来支持我的工作…
想联系我吗?
我很想听听你对这个话题,或者对任何 AI 和数据的看法。
如果你希望联系我,请发邮件到davidfarrugia53@gmail.com。
探索大语言模型的领域
对于前瞻性领导者和企业家而言的选择和考虑
·
关注 发布于 Towards Data Science ·6 分钟阅读·2023 年 7 月 24 日
–
图片: 大卫·科尔布
微软和 Meta 最近推出了 Llama 2,这是一种下一代开源大语言模型(LLM)。有了 Llama 2 的广泛预训练和微调 LLM 的集合,企业现在面临一个关键问题。
这一点对寻求采用大语言模型的前瞻性领导者和企业家有什么影响?
随着大语言模型(LLM)市场变得越来越复杂,公司面临五种选择:商业模型、开源模型、微调模型、定制模型,或者与 AI 提供商/研究人员合作。
针对 LLM 市场日益复杂的情况,本文旨在总结企业可用的五种主要选项。
商业模型
商业模型,如 ChatGPT、Google Bard 和 Microsoft Bing,为有远见的领导者和企业家提供了直接、高效的解决方案。这些模型已经在多样化的数据集上进行了广泛的训练,具备文本生成、语言翻译和问答能力。它们的关键优势在于即时可用性。通过正确的策略、程序和流程,企业可以快速部署这些模型,迅速利用其能力。
然而,必须记住,尽管这些模型设计为多用途,服务于广泛的应用,它们可能不擅长于特定于您业务的任务。因此,应根据您的独特业务需求考虑它们的适用性。
开源模型
开源模型是考虑实施大型语言模型(LLM)解决方案的企业的一个经济选择。这些免费提供的模型具有先进的语言能力,同时能够降低成本。然而,需要注意的是,开源模型可能无法提供与专有选项相同的控制级别,特别是对于需要广泛定制的组织。
在某些情况下,它们使用的数据集比商业模型更小。开源 LLM 仍然在文本生成、翻译和问答任务中提供多样性。开源模型的主要优势在于其成本效益。若干开源供应商提供微调服务,以满足特定业务需求,提供更为量身定制的方案。
一个需要考虑的因素是开源模型的维护和支持。公共云服务提供商通常会更新和改进其商业模型,而开源模型可能缺乏持续的维护。必须评估所选开源模型的可靠性和持续发展,以确保长期适用性。
微调模型
微调模型使企业能够在特定业务任务上实现最佳性能。这些模型结合了商业模型的优势,通过使用组织的数据进行额外训练来提升性能。
一个希望改善客户支持聊天机器人的公司可以从能够理解和生成自然语言的商业模型开始。他们可以使用历史客户支持聊天记录对该模型进行微调,以训练它处理特定的客户查询、回应和上下文。
微调的优势在于能够根据特定需求调整模型,同时受益于商业模型提供的易用性。这对行业特定的术语、独特的要求或专业的用例尤其有价值。然而,微调可能资源密集,要求有一个准确代表目标领域或任务的合适数据集。获取和准备这个数据集可能涉及额外的成本和时间。
在精心执行的情况下,微调使企业能够将大型语言模型适应其独特需求,提升性能和任务特定的相关性。尽管涉及规划和投资,但其好处使微调模型对旨在提升语言处理能力的组织具有吸引力。
图片 : David Kolb
构建定制模型
从头开始构建定制的 LLM 为企业提供了无与伦比的控制和定制性,但成本更高。此选项复杂,需要机器学习和自然语言处理方面的专业知识。定制 LLM 的优势在于其量身定制的特性。它可以根据你的业务的独特需求进行设计,确保性能最佳且与目标一致。
使用定制的 LLM,你可以控制模型的架构、训练数据和微调参数。然而,构建定制的 LLM 既耗时又昂贵。它需要一个熟练的团队、硬件、大量研究、数据收集和注释以及严格的测试。还需要进行持续的维护和更新,以保持模型的有效性。
构建定制的 LLM 是寻求绝对控制和高性能组织的终极选择。虽然这需要投入,但它为你的语言处理需求提供了高度量身定制的解决方案。
混合方法
混合方法结合了不同策略的优势,提供了平衡的解决方案。企业可以通过结合商业模型和微调或定制模型来实现定制化和高效的语言模型策略。
该方法经过优化,以满足任务特定的要求和行业细节。例如,当新客户请求进来时,商业模型可以处理文本并提取相关信息。这一初步互动受益于商业模型的一般语言理解和知识。经过微调或定制的模型,专门针对企业的客户互动和对话数据进行训练,将接管分析处理过的信息,提供量身定制和有针对性的响应,利用其在客户评价和类似互动上的训练。
通过采用混合方法,企业可以实现一种适应性强且高效的策略,提供量身定制的解决方案,同时利用商业模型中的知识。这种策略在既定语言模型的背景下,为满足业务特定需求提供了一种实用而有效的方法。
图片 : 大卫·科尔布
与 AI 供应商的合作
与 AI 供应商合作是实施 LLM 的企业的一个可行选择。这些供应商提供专业知识和资源来构建和部署量身定制的语言模型。与 AI 供应商合作的优势在于可以获得他们的专业知识和支持。他们拥有深厚的机器学习和自然语言处理知识,有效指导企业。他们提供见解、推荐模型,并在开发和部署过程中提供支持。请考虑,与 AI 供应商合作可能会涉及额外费用。评估其财务影响。
通过与 AI 供应商合作,企业可以受益于专业知识,确保 LLM 的更顺利集成。虽然需要考虑费用,但与 AI 供应商合作的优势,特别是在专业指导和支持方面,可能会超过费用支出。
结论
在快速发展的生成式 AI 领域,做出正确选择不仅需要了解可用的模型,还需要知道每个模型如何与您的独特业务目标对齐。
这里有一些关键要点
-
大型语言模型有可能彻底改变业务运作和客户互动,但利用这一潜力需要一个与您特定需求相一致的策略。
-
成功实施这些模型并非偶然——这是一个选择。这取决于您是否能够采纳整体视角,在即时需求与未来趋势和机会之间取得平衡。
-
没有一种适合所有人的解决方案。最佳策略将是为您的业务量身定制的方案。
在考虑这些见解时,请思考一下:在复杂的生成式 AI 领域,最大的挑战往往不是技术本身,而是确定合适的策略以释放其潜力。有时,困惑与清晰、停滞与进步之间的差距,仅在于是否得到正确的指导。
欲了解更多信息,请访问 David Kolb Consultancy
使用 PySpark 的 NBA 分析
原文:
towardsdatascience.com/nba-analytics-using-pyspark-a1699ae1117a
背靠背比赛的胜率、比赛得分的均值和标准差,以及更多的 Python 代码
·发表于 Towards Data Science ·7 min read·2023 年 4 月 14 日
–
图片由 Emanuel Ekström 提供,来源于 Unsplash
动机
一周多前,我观看了一场 NBA 比赛,密尔沃基雄鹿对阵波士顿凯尔特人。这是联赛前两名球队的对决,许多人认为这场比赛是东部决赛的前奏。作为一个篮球和 NBA 的狂热粉丝,这场比赛令人相当失望,因为密尔沃基雄鹿以 99–140 输给了波士顿凯尔特人,这是密尔沃基在 2022–2023 赛季中为数不多的惨败之一,而密尔沃基在常规赛中持有最佳战绩。
尽管这对于密尔沃基来说显得有些不寻常,尤其是考虑到这是在主场的惨败,但比赛的评论员提醒我他们实际上是在打背靠背比赛,即在前一天比赛之后进行的比赛(在这种情况下,是前一天在印第安纳的客场比赛)。换句话说,疲劳可能在他们的失利中发挥了作用,因为背靠背比赛对运动员身体要求很高,加上比赛间的旅行(从印第安纳回到密尔沃基)可能加剧了这一点。
查看球队赛程,在一个赛季的 80 多场比赛中,NBA 球队确实会进行多场背靠背比赛。你是否曾经好奇这些比赛中球队的表现如何,以及当球队在客场或主场比赛时这种情况是否有所变化?本文展示了使用 PySpark — 一个即用型的 Python Apache Spark 接口 — 来获取这些通常在公共领域中不可用的统计数据的一种方法。
数据
为了确定背靠背比赛的胜率,我们需要每支 NBA 球队背靠背比赛的历史记录及其结果。虽然这些统计数据可以在官方 NBA 网站和其他社区网站上找到,但它们不允许商业使用,因此我模拟了一个包含以下字段的合成数据集。
-
比赛进行的日期
-
主队名称
-
客队名称,以及
-
比赛分数,以及主客队的相应结果
下表显示了合成数据集的一个片段。你应该能够通过官方 NBA 比赛时间表核实这些并不是实际的比赛。
表 1:合成比赛数据。作者提供的表格。
数据转换
本节提供了一个逐步的 Python 指南,讲解如何将上述数据集转换为识别某场比赛是否为背靠背比赛的数据集,并随后计算每支球队这些比赛的胜率。
步骤 1:加载包和数据
#Load required Python packages
import numpy as np
import pandas as pd
!pip install pyspark #Install PySpark
import pyspark
from pyspark.sql.window import Window #For use of Window Function
from pyspark.sql import functions as F #For use of Window Function
from pyspark.sql import SparkSession #For initiating PySpark API in Python
#Read in game.csv
path_games = "/directory/game_synthetic.csv" #Replace with your own directory and data
data_raw_games = pd.read_csv(path_games, encoding = 'ISO-8859-1')
步骤 2:格式化并创建日期列
#Format the 'game_date' column (if it was defaulted to string at ingestion)
#into Date format
data_raw_games['GAME_DATE'] = pd.to_datetime(data_raw_games['game_date'], \
format='%Y-%m-%d')
#Create a 'GAME_DATE_minus_ONE' column for each row
data_raw_games['GAME_DATE_minus_ONE'] = pd.DatetimeIndex(data_raw_games['GAME_DATE']) \
+ pd.DateOffset(-1)
上述创建的‘GAME_DATE_minus_ONE’列表示数据集中每场比赛的前一个日历日期。这将在稍后(步骤 4)详细讨论,并用于识别是否为背靠背比赛。
步骤 3:按队伍分割数据集
由于数据集的每一行都处于比赛级别(即显示两队之间的比赛结果),因此需要进行分割以在队伍级别上表示结果(即将每一行分割为两个,表示每队的比赛结果)。可以使用下面的 Python 代码实现这一点。
#Create two dataframes, one for results of home teams and
#one for results of away teams, and merge at the end
data_games_frame_1 = data_raw_games.sort_values(['game_id'])
data_games_frame_2 = data_raw_games.sort_values(['game_id'])
data_games_frame_1['TEAM_ID'] = data_games_frame_1['team_id_home']
data_games_frame_2['TEAM_ID'] = data_games_frame_2['team_id_away']
data_games_frame_1['WIN_FLAG'] = (data_games_frame_1['win_loss_home'] == 'W')
data_games_frame_2['WIN_FLAG'] = (data_games_frame_1['win_loss_home'] != 'W')
data_games_frame_1['TEAM_NAME'] = data_games_frame_1['team_name_home']
data_games_frame_2['TEAM_NAME'] = data_games_frame_2['team_name_away']
data_games_frame_1['TEAM_NAME_OPP'] = data_games_frame_1['team_name_away']
data_games_frame_2['TEAM_NAME_OPP'] = data_games_frame_2['team_name_home']
data_games_frame_1['HOME_FLAG'] = 'Home'
data_games_frame_2['HOME_FLAG'] = 'Away'
#Merge the two dataframes above
data_games = pd.concat([data_games_frame_1, data_games_frame_2], axis = 0).drop(['team_id_home', 'team_id_away'], axis = 1)\
.sort_values(['game_id']).reset_index(drop = True)
步骤 4:返回每场比赛的日期,标记出该队上一次比赛的日期
这时,PySpark 就显得特别有用。特别是,我们将利用 PySpark 中窗口函数下的lag函数。实际上,如下面的 表 2 所示,lag函数提供了对所选列的偏移值的访问。在这种情况下,它返回亚特兰大老鹰队相对于当前比赛的上一场比赛的日期,通过一个窗口,该窗口显示了亚特兰大老鹰队所有的比赛记录。
例如,在索引 1 的行中,亚特兰大老鹰队在 23/10/2021 与克利夫兰骑士队进行了比赛(“当前比赛”),如‘GAME_DATE’列所示,其上一场比赛是在 21/10/2021 对阵达拉斯小牛队,如‘GAME_DATE’列所示,该数据通过lag函数在当前比赛所在的行中返回,显示在“GAME_DATE_PREV_GAME”列。
表 2:lag函数演示。作者提供的表格
上面返回的‘GAME_DATE_PREV_GAME’列,当其等于在步骤 2中创建的‘GAME_DATE_minus_ONE’列时,表示这是一场连续比赛(即上场比赛的日期等于当前比赛的前一个日历日)。这在表 1 中索引 8(和 14)的行中是这样的,因为亚特兰大老鹰队在 2021 年 4 月 11 日对阵犹他爵士队——在 2021 年 3 月 11 日对布鲁克林篮网队之后的一天。
返回‘GAME_DATE_PREV_GAME’列以及标记所有球队的连续比赛的 Python 代码如下所示。
#Select relevant columns from the dataset
col_spark = [
'GAME_DATE'
,'GAME_DATE_minus_ONE'
,'TEAM_ID'
,'TEAM_NAME'
,'TEAM_NAME_OPP'
,'HOME_FLAG'
,'WIN_FLAG'
,'SCORE'
,'season_id'
]
df_spark_feed = data_games[col_spark]
#Initiate PySpark session
spark_1= SparkSession.builder.appName('app_1').getOrCreate()
df_1 = spark_1.createDataFrame(df_spark_feed)
#Create window by each team
Window_Team_by_Date = Window.partitionBy("TEAM_ID").orderBy("GAME_DATE")
#Return date of previous game using the lag function
df_spark = df_1.withColumn("GAME_DATE_PREV_GAME", F.lag("GAME_DATE", 1).over(Window_Team_by_Date)) \
#Flag back-to-back games using a when statement
.withColumn("Back_to_Back_FLAG", F.when(F.col("GAME_DATE_minus_ONE") == F.col("GAME_DATE_PREV_GAME"), 1) \
.otherwise(0))
#Convert Spark dataframe to Pandas dataframe
df = df_spark.toPandas()
步骤 5:计算连续比赛的胜率
#Select relevant columns
col = [
'TEAM_NAME'
,'TEAM_NAME_OPP'
,'GAME_DATE'
,'HOME_FLAG'
,'WIN_FLAG'
]
#Filter for back-to-back games
df_b2b_interim = df[df['Back_to_Back_FLAG'] == 1]
#Show selected columns only
df_b2b = df_b2b_interim[col].sort_values(['TEAM_NAME', 'GAME_DATE']).reset_index(drop = True)
各队连续比赛的胜率是多少?
表 3:按球队的连续比赛胜率。表格由作者提供
基于合成数据集,似乎连续比赛的胜率因球队而异。休斯顿火箭队在连续比赛中的胜率最低(12.5%),其次是奥兰多魔术队(14.8%)。
连续比赛是在客场还是主场进行的重要吗?
表 4:按球队和主客场的连续比赛胜率。表格由作者提供
基于合成数据集,似乎在表 4中的大多数球队,球队在主场赢得连续比赛的可能性更大,而不是在客场(这是一个合理的观察)。布鲁克林篮网队、芝加哥公牛队和底特律活塞队是这一观察结果的少数例外。
其他拆分也可以计算,例如使用下面的 Python 代码计算连续比赛与非连续比赛的胜率。输出的片段表明,球队更有可能赢得非连续比赛(这仍然是一个合理的观察,尽管有一些例外)。
表 5:连续比赛与其他比赛的胜率。表格由作者提供
其他统计
在步骤 4中使用的 PySpark 会话及相关窗口函数可以进一步自定义,以返回其他比赛统计数据。
例如,如果我们想按赛季查询胜率(无论是连续比赛还是非连续比赛),只需按球队和赛季 ID 引入一个窗口,并像下面这样对其进行分区。
#Create window by season ID
Window_Team_by_Season = Window.partitionBy("TEAM_ID").orderBy("season_id")
此外,我们都知道 NBA 比赛的得分波动性很大,但究竟有多大?这可以通过得分的标准差来衡量,而这在公共领域中可能不可用。我们可以通过引入数据集中可用的得分,并应用 avg 和 stddev 窗口函数来轻松校准,这将返回预定义窗口上的标准差。
举例来说,如果 NBA 比赛的标准差约为 20 分,那么有 70% 的概率得分会在 NBA 比赛平均得分线的 +/- 20 分以内(假设正态分布)。
返回该统计数据的示例 Python 代码如下所示。
spark_1= SparkSession.builder.appName('app_1').getOrCreate()
df_1 = spark_1.createDataFrame(df_spark_feed)
Window_Team = Window.partitionBy("TEAM_ID").orderBy("HOME_FLAG")
df_spark = df_1.withColumn("SCORE_AVG", F.avg("SCORE").over(Window_Team)) \
.withColumn("SCORE_STD", F.stddev("SCORE").over(Window_Team))
df = df_spark.toPandas()
df.groupby(['TEAM_NAME', 'HOME_FLAG'])["SCORE_AVG", "SCORE_STD"].mean()
当我乘风破浪于 AI/ML 领域时,我喜欢以通俗易懂的语言编写和分享逐步指导和操作教程,并附有可运行的代码。如果你想访问我所有的文章(以及来自其他实践者/作者在 Medium 上的文章),你可以通过 这个链接 进行注册!
最近邻回归器 — 可视化指南
原文:
towardsdatascience.com/nearest-neighbors-regressors-a-visual-guide-78595b78072e
模型的视觉理解及超参数的影响
·发布于 Towards Data Science ·阅读时间 8 分钟·2023 年 3 月 31 日
–
K 最近邻(KNN)是机器学习中最简单的模型之一。实际上,在某种程度上,它没有模型,因为对于新观测的预测,它将使用整个训练数据集来根据距离(通常是欧几里得距离)找到“最近邻居”。然后在回归任务中,预测值是通过对这些邻居的目标变量值取平均来计算的。
由于我们使用的是距离的概念,所以应该只使用数值特征。当然,你可以通过独热编码或标签编码转换分类特征,距离计算算法仍然可以工作,但距离可能会变得没有意义。
还值得注意的是,目标变量的值并未用于寻找邻居。
在本文中,我们将使用一些简单的数据集来可视化 KNN 回归器的工作原理,以及超参数 k 如何影响预测。我们还将讨论特征缩放的影响。我们还将探索一个较少为人知的邻近版本,即半径最近邻。最后,我们将讨论更自定义的距离版本。
一个连续特征
我们将使用一个具有非线性行为的简单数据集,因为我们知道 KNN 能够处理这种情况。
最近邻回归器数据集 — 作者提供的图片
对于那些读过我关于 决策树回归器 可视化文章的人,你可以注意到这就是相同的数据。我们将与决策树回归器模型进行快速比较。
我们可以创建并拟合一个 KNeighborsRegressor 模型,使用 KNeighborsRegressor(n_neighbors = 3),然后用 model.fit(X, y) 来“拟合”模型。
为了使所有模型的拟合过程相同,你可以注意到模型是通过经典的拟合方法“拟合”的。但对于 KNeighborsRegressor,拟合过程仅仅是保存数据集 X 和 y,没有其他内容。是的,这是最快的拟合!模型也是最大的一次!
现在,我们可以测试一个观测值的“模型”。在以下代码中,我们将使用一个单点。经典预测方法是计算预测值,而 kneighbors 方法允许我们获取邻居。
然后我们可以绘制邻居。我将展示 x = 10 和 x = 20 的图形。请随意进行更多测试。
最近邻回归器与 kneighbors — 图片作者
现在,我们也可以使用一系列 x 值来获取所有预测结果。
这里是前述代码生成的结果图。对于红色段上的每一个点,y 值代表 k 个最近邻的平均值(这里 k = 3)
带预测的最近邻回归器 — 图片作者
现在,让我们为不同的 k 值创建模型预测。
不同 k 值下的最近邻回归器 — 图片作者
我们还可以与决策树回归模型进行比较
最近邻回归器与决策树回归器 — 图片作者
我们可以注意到,对于决策树回归器,边界总是干净利落的,而对于 k 最近邻回归器,边界则更加细腻。
两个连续特征
我们将使用以下数据集,具有两个连续特征,来创建一个 KNN 模型。对于测试数据集,我们将使用 meshgrid 生成网格。
然后我们可以使用 plotly 创建交互式 3D 图。在下图中,我们可以看到不同 k 值的 3D 图。
带有两个特征的最近邻回归器 — 图片作者
在这里,我们可以再次与决策树回归模型进行比较。我们可以看到并感受到这两个模型的行为差异。
最近邻回归器与决策树回归器 — 图片作者
缩放的影响
与决策树不同,特征的缩放对模型有直接影响。
例如,对于两个特征的情况,我们可以进行以下变换。值得注意的是,对于一个连续特征,缩放没有影响,因为相对距离没有变化。
不同特征尺度下的最近邻回归器 — 图片作者
我们可以直观地得出这两组模型非常不同。我们可以计算通常的模型性能来比较它们。但在这里,我的方法确实是通过视觉上展示模型如何表现不同。你能感受到吗?距离发生了变化,因为特征的尺度发生了变化。最终,邻居也发生了变化。
有人可能会说我们应该使用标准化或最小-最大缩放。但你可以看到,上面的图像中,某些情况可能是标准化(或最小-最大缩放)数据集。而你无法事先判断标准化是否有助于模型性能的提升。
实际上,为了考虑每个特征在距离计算中的相对重要性,我们应该为不同的特征赋予不同的权重。但这会使调优过程变得过于复杂。
想象一下在线性回归模型中,关键是为每个特征找到系数。在 k NN 的距离计算中,所有特征被视为同等重要。从直觉上讲,我们可以感受到这个 kNN 模型的表现不会太好!
半径邻居
在 scikit-learn 的 [neighbors](https://scikit-learn.org/stable/modules/classes.html#module-sklearn.neighbors)
模块中,有一个鲜为人知的模型叫做 RadiusNeighborsRegressor,你可以从它的名字轻易理解到,与 K 近邻模型不同,我们使用一个固定半径的圆圈来围绕新的观察点找到它的邻居。
现在,何时半径邻居模型可能会更有趣?让我们以一个包含异常值的数据集为例。我们可以看到这两种模型的表现不同。由于这个异常值“远”离其他点,在 KNN 的情况下,邻居数量是固定的,因此邻居之间也相距较远。但对于半径邻居,异常值的影响更为重要。
最近邻回归器 KNN vs. 半径 NN — 图片由作者提供
在这里请注意,当我使用“异常值”一词时,并不一定意味着我们应该去除这个异常值。我只是想展示一些数据点远离其他点的情况。
影响对于非异常值也同样显著。因为我们无法满足这两种情况。
当半径过小时,你可能会注意到上图中有些奇怪的现象。是的,在这种情况下,对于某些点,没有邻居。然后,分配了一个巨大的负值。事实上,在这种情况下没有解决方案。但我仍然认为这是一个应该在 scikit-learn 中纠正的错误。产生错误总比默认给出这个值要好。
在完成半径邻居之前,什么时候它会真正有趣?想象一下,当你在城市的一个区域有大量数据,而在附近的另一个区域数据较少,但你知道你本可以收集更多数据。此时半径邻居可能更相关。
类似地,如果你为一个区域有一个值(将分配给该区域的所有地址),那么我们可以使用邻居来平滑该值。这里再次,半径邻居将更相关。下面是使用最近邻模型进行值平滑的示例。
地理邻居平滑 — 作者图像
关于距离的更多信息
关于距离的最后一个有趣讨论,你可能在可视化前述制图时已经考虑过。距离的概念可能非常具体,因为如果你用经纬度计算欧几里得距离,那么这种距离可能无法正确反映地理邻域(这是你可能希望使用的距离)。
你可能已经知道,但看到总是更好。从下面的图像中,红色圆圈是中央位置的“真实圆”,由与中央位置具有相等地理距离的位置形成的红色区域。蓝色“圆圈”是通过计算经纬度的欧几里得距离得到的。在赤道附近,这两个圆圈几乎相同。但在远离赤道的地方,它们会有很大不同。因此,下次你在数据集中使用经纬度并使用最近邻模型时,你必须考虑这一点。
地球上的真实圆形与经纬度的“圆形” — 作者图像
现在,你可以想象在其他情况下,更自定义的距离可能是必要的。因此,这种简单模型可能会变得更高效。
邻居的加权也是可能的。你可以使用weights参数来实现。以下是来自官方文档对该参数的描述:
weights : {‘uniform’, ‘distance’}, callable 或 None, 默认=‘uniform’
预测中使用的权重函数。可能的值:
‘uniform’ : 均匀权重。每个邻域中的所有点都被赋予相等的权重。
‘distance’ : 通过距离的倒数来加权点。在这种情况下,查询点的较近邻居将比较远的邻居具有更大的影响。
[callable] : 用户定义的函数,该函数接受一个距离数组,并返回一个包含相同形状的权重数组。
默认使用均匀权重。
K 最近邻回归器,权重 = “distance” — 作者图像
然而,距离设计可能变得非常复杂,这时采用其他方法如决策树和基于数学函数的模型可能更为简单。
结论
我正在撰写一系列类似的文章,演示如何通过可视化帮助我们更好地理解机器学习模型的工作原理而无需数学。请通过下面的链接关注我,获取我文章的全部访问权限:medium.com/@angela.shi/membership
如果你想获取生成本文图形的代码,你可以在这里支持我:ko-fi.com/s/4cc6555852
因此,在本文中,我们展示了最近邻“模型”对于简单数据集来说,借助可视化是相当直观的,因为邻居的概念是直观且简单的。
超参数 K 或半径的选择对最近邻模型的性能有重要影响。如果 K(或半径)太小,模型可能会过拟合数据中的噪声;而如果 K(或半径)太大,模型可能会欠拟合,无法捕捉数据中的潜在模式。
当使用最近邻模型时,数据的缩放也很重要,因为该算法对输入特征的缩放非常敏感。
需要速度:将 Pandas 2.0 与四个 Python 加速库进行比较(附代码)
Polars、Dask、RAPIDS.ai cuDF 和 Numba 与使用 pyarrow 的 Pandas 2.0 在后台处理、向量化以及 itertuples()、apply() 方法上进行了比较。
·发布于 Towards Data Science ·阅读时间 9 分钟·2023 年 5 月 19 日
–
图片由 alan9187 提供,来源于 Pixabay
到 2023 年为止,这一年给机器学习领域带来了显著的进展,先进的大语言模型(LLMs)得到了开发和全球分布。然而,机器学习的专业能力不仅限于对LLLMs的微调和提示工程。一位机器学习专家了解系统内部的运作,能够解释/优化各种系统行为,并最终负责机器学习解决方案的整体质量、业务需求适配性和性能。
除了性能比较,文章还旨在通过提供代码示例来教育读者如何实现各种 Python 加速操作,突出关键点。
说到了解系统内部运作和性能,本文将对比新发布的 Pandas 2.0 与四个其他加速 Python 库和操作:Polars、RAPIDS.ai cuDF、Dask 和 Numba。代码在 ASUS Rig Strix Scar(2022 款)游戏笔记本电脑上执行,配置为 NVIDIA GeForce RTX 3080 Ti Laptop GPU 和 Intel Core i9 12900Hz 处理器。除了性能比较,文章还旨在通过提供代码示例来教育读者如何实现各种 Python 加速操作,突出关键点。
在我们的代码示例中,我们将使用一个合成的*.csv文件,该文件包含 500K 行,模拟一个超级商店的两地商品价格。文件有三列;第一列,称为Store1*,包含第一个超级商店位置的库存价格,第二列,称为Store2,包含第二个超级商店位置的库存价格。Store2的价格略高(最高可高 20%)。最后,第三列,称为Discountability,仅包含 0 和 1,指示一个项目是否可以打折。0 表示该项目不能打折,1 表示可以打折。创建Pandas 2.0 DataFrame和合成*.csv*文件的代码如下。
关键点:注意到col2(Store2的价格)是通过使用矢量化操作从numpy数组col1和var创建的,而无需使用循环。我们之所以能这样做,是因为 NumPy支持矢量化操作。
A. Dataframe 创建性能比较
我们将比较不同方法性能的第一个操作是DataFrame的创建。具体来说,我们将比较Pandas 2.0、Dask和Polars的性能。在接下来的两个部分中,我们将讨论代码实现,然后在 A.3 节中,我们将比较性能。
A.1. 使用 Pandas 2.0
Pandas 2.0中的一个重要新特性是添加了 Apache Arrow (pyarrow)支持的内存格式。Apache Arrow的主要优点是它可以执行更快、更节省内存的操作。下面的代码展示了使用和不使用pyarrow构建Pandas DataFrame的方式。
关键点: 注意,一旦我们将后端类型(dtype_backend)指定为pyarrow,我们还需要将engine指定为pyarrow。
A.2 使用 Polars 和 Dask
Polars是一个加速的 Python DataFrame库,实施了Apache Arrow内存模型,并用Rust编写。与Apache Arrow类似,它既快速又内存高效,特别适合数据密集型计算。为了创建Polars DataFrame,我们只需导入Polars库并调用其*read_csv()*方法。
Dask是一个开源框架,支持 Python 中的并行和分布式计算,因此特别适合数据密集型应用程序。为了创建一个Dask DataFrame,我们必须首先导入dask.dataframe库,然后调用其*read_csv()*方法。
关键点。 安装Dask库时要小心,因为Pandas是其依赖项之一,它会安装它。在撰写本文时,Dask的安装不会安装Pandas 2.0,而是一个较旧的版本。因此,如果你想使用Dask而不覆盖你的Pandas 2.0安装,请创建一个虚拟环境并在其中安装Dask。
A.3 Dataframe 创建执行时间比较
操作时间使用timeit库进行实现,如下所示。
下方展示了从最快到最慢的DataFrame创建执行时间。最快的是Polars和Dask,紧随其后的是具有pyarrow后端的Pandas 2.0。至于传统的Pandas DataFrame创建,它比前者慢 4.37 倍。因此,即使从创建Pandas DataFrame的第一步开始,pyarrow也能发挥作用。
-
使用Dask:0.020656200009398162
-
使用Polars:0.027874399966094643
-
使用pyarrow支持的Pandas 2.0:0.04491699999198317
-
使用Pandas 2.0(不使用pyarrow):0.196299700008239
除了测量执行时间,我还进行了一些内存使用的估算。cuDF DataFrame的内存占用最小(0.12MB),而Polars DataFrame的内存占用约为 7.63MB,Pandas DataFrame的内存占用约为 11.44MB。注意,cuDF DataFrame的创建时间未提供,因为*.csv文件存储在 CPU 中,因此我们无法利用cuDF*的 GPU 能力进行此操作。我们将在文章后面讨论这一点。
B. DataFrame操作的性能比较
为了比较上述库在执行DataFrame操作时的性能,假设如下情况:所有Store1的合格商品将折扣 20%,所有Store2的合格商品将折扣 30%,折扣后的价格将保存在一个新的数据框中。我们使用“合格”这个词,因为如上所述,Discountability为 0 的项目不能享受折扣。因此,我们将对应用函数到DataFrame的行上进行执行时间比较,这是一个常见任务。
代码实现展示在 B.1-B.8 节,性能比较展示在 B.9 节。
B.1 使用 pyarrow 的 Pandas
在我们对DataFrame操作的第一次实验中,我们将利用Apache Arrow的能力,鉴于它最近与Pandas 2.0的互操作性。如下面代码的第一行所示,我们将一个Pandas DataFrame转换为pyarrow Table,这是一种在内存中高效表示列数据的方式。每一列单独存储,这允许高效的压缩和数据查询。然后,将Store1、Store2和Discountability列传递给scale_columns()函数,该函数通过适当的折扣(0.2 或 0.3)和Discountability列的掩码值(0 或 1)来缩放这些列。缩放后的列作为元组由函数返回。最后,将表result_table转换为Pandas DataFrame。
关键点: 在函数scale_columns()中,乘法操作是通过pyarrow.compute* 函数multiply() 实现的。注意每一个乘法操作都必须使用multiply() 实现。例如,如果我们将上面的pc.multiply(0.2,mask_col) 替换为pc.multiply(0.2mask_col),我们将会遇到错误。最后,我们使用pyarrow.compute* 的*subtract()*函数进行减法。
B2. 使用 Pandas 方法 apply()
我们的第二个实验将使用Pandas DataFrame的*apply()方法来执行逐行操作。函数scale_columns()在下面的代码中执行缩放。请注意,实际的缩放是在嵌套函数discount_store()*中完成的。
关键点: 嵌套函数返回一个包含字典的Pandas Series,其中key是列名,value是缩放后的商店价格。你可能会好奇为什么我们要返回这种类型的对象。原因是,Pandas apply() 方法返回一个Series,其索引是列名。因此,将结构类似于其返回值的对象传递给apply()有助于计算。为了教育目的,在我的代码的github目录中(文章末尾有链接),我提供了两个使用*apply()的实现。这里展示的是其中一个,还有一个实现,其中嵌套函数将缩放值作为元组返回。这个实现计算上更为密集,因为返回的类型与apply()*需要返回的结构不同。
B.3. 使用 Pandas itertuples()
itertuples() 是一个快速的Pandas 行迭代器,生成namedtuples(列名,相应行的值)。实现商店价格缩放并使用itertuples() 计算折扣价格的代码如下所示。
关键点: Pandas itertuples() 方法通常比 Pandas apply() 更快,特别是对于较大的数据集,就像我们的代码示例一样。原因是*apply()为每一行调用一个函数,而itertuples()*则不会,并且可以利用向量化操作,同时返回一个轻量级迭代器,这不会创建新对象,且内存效率高。
B.4 Pandas 向量化操作
现在,我们已经到了计算折扣商店价格的最优雅和最快的方法:向量化!这是一种允许对整个数组一次性应用所需操作的方法,而不是使用循环进行迭代。实现如下所示。
B.5 使用 Numba
Numba 是一个即时(JIT)编译器,用于将 Python 代码在运行时翻译成优化的机器代码。它在优化涉及循环和NumPy 数组的操作中非常有用。
@numba.njit 符号是一个装饰器,它告诉Numba 随后的函数要编译成机器代码。
B.6 使用 Dask
类似于NumPy,Dask提供了向量化操作,额外的优势是这些操作可以以并行和分布的方式应用。
Dask的额外优势包括:(a) 懒惰计算,即Dask数组和操作建立在任务图上,仅在请求结果时执行,例如调用compute()。(b) 超出内存处理,即可以处理不适合内存的数据集。© 与Pandas的集成。
B.7 使用 Polars
Polars提供了利用Rust语言的速度和内存效率的向量化操作。类似于Dask,它提供了懒惰计算、超出内存处理和与Pandas的集成。实现如下所示。
B.8 使用 RAPIDS.ai cuDF
最后,我们使用cuDF通过向量化操作实现了价格折扣功能。cuDF库建立在CUDA之上,因此其向量化操作利用了 GPU 的计算能力以加快处理速度。cuDF的一个方便功能是提供CUDA kernels。这些是优化的函数,利用了 GPU 的并行特性。cuDF的实现如下所示。
B.9 函数应用的执行时间比较
类似于DataFrame的创建时间,timeit被用来测量价格折扣功能的执行时间及其在新DataFrame中的节省。下面是执行时间,按从最快到最慢排序。
-
使用RAPIDS.ai, cuDF: 0.008894977000011295
-
使用Polars: 0.009557300014421344
-
使用pyarrow Table: 0.028865800006315112
-
使用Pandas 2.0向量化操作: 0.0536642000079155
-
使用Dask: 0.6413181999814697
-
使用Numba: 0.9497981000000095
-
使用Pandas 2.0 itertuples(): 1.0882243000087328
-
使用Pandas 2.0 apply(): 197.63155489997007
不出所料,支持 GPU 的Rapids.ai cuDF取得了最快的时间,其次是闪电般快速的Polars库。pyarrow Table和Pandas 2.0的向量化操作的执行时间也紧随其后;cuDF的执行时间平均比后两者快约 4.64 倍。Dask、Numba和Pandas的itertuples()方法的表现较差但仍合理(分别比Polars慢约 72、107 和 122 倍)。最后,Pandas的apply()相对于所有其他方法表现极其差,这也不令人惊讶,因为此方法在处理大型数据集时以逐行方式操作,速度相对较慢。
关键点:(a)一般来说,apply()是对小到中型数据集上的Pandas DataFrames应用函数的优秀方法。但是,当处理大型数据集时,如我们所例,最好考虑其他实现函数的方法。(b)如果你决定在Pandas 2.0中使用apply(),请确保这在标准创建的DataFrame上进行,而不是在后台使用pyarrow的。原因是,显著的数据类型转换开销会大大减慢你的计算速度。
C. 结论
在这篇文章中,我描述了几种加速应用于大型数据集的 Python 代码的方法,特别关注新发布的Pandas 2.0和pyarrow后端。不同的加速技术在两个任务中进行了性能比较:(a)DataFrame创建和(b)对DataFrame行应用函数。对于Pandas 2.0用户来说,确实是好消息; Pandas DataFrame的向量化操作和pyarrow Table(从Pandas DataFrame中提取)实现了与Rapids.ai cuDF和Polars库相当的性能。
整个代码在github目录下:link
感谢阅读!
嵌套字典 Python——Python 嵌套字典的完整指南
如何在 Python 中使用嵌套字典?本文将教你关于 Python 嵌套字典的一切知识。
·发表于数据科学的前沿 ·12 分钟阅读·2023 年 4 月 18 日
–
什么是 Python 中的嵌套字典?
Python 中一种常见的数据结构是嵌套字典,或者说字典的值可以是其他字典。初学者不喜欢嵌套字典,因为它们需要更多的时间来处理和正确解析,但只要稍加练习,你就能掌握它。
今天你将学习什么是嵌套字典,为什么在 Python 中使用嵌套字典,如何在 Python 中遍历嵌套字典,等等。关于库的导入,将其放在你的脚本或笔记本的顶部:
import pprint
pp = pprint.PrettyPrinter(depth=4)
它将在打印嵌套字典时处理格式,使其更易于阅读。
如何在 Python 中创建嵌套字典
有许多方法可以创建嵌套字典,但如果你从头开始在 Python 中创建它们,你主要会使用两种方法。
使用常规 Python 符号
创建嵌套 Python 字典的第一种方法是利用常规 Python 符号。这意味着你不需要使用任何特定的函数或库来创建字典。只需将其分配给一个变量名,并将整个内容格式化为 JSON。
这是一个示例——以下代码片段创建了一个员工的嵌套字典,其中员工的电子邮件被用作字典的键,附加信息作为字典值。正如你所看到的,字典值本身也是一个字典:
employees = {
"bdoe@email.com": {
"first_name": "Bob",
"last_name": "Doe",
"address": {
"city": "New York",
"street": "1st Street",
"house_number": 1
}
},
"mmarkson@email.com": {
"first_name": "Mark",
"last_name": "Markson",
"address": {
"city": "San Diego",
"street": "2nd Street",
"house_number": 2
}
}
}
pp.pprint(employees)
这就是这个嵌套字典的样子:
图像 1 — 员工的嵌套字典(作者提供的图像)
总体而言,我们有一个包含两个键(电子邮件)的字典。每个键都有一个字典作为值,甚至还有一个第三个字典分配给address
键。
字典是无序的,所以你看到的数据没有反映代码中指定的排序。对此不必担心。
使用 zip() 函数
在 Python 中创建嵌套字典的另一种方法是使用zip()
函数。它用于同时迭代两个或多个迭代器。
为了演示,我们将声明两个列表:
-
employee_emails
- 一个电子邮件列表,将作为字典的键 -
employee_details
- 每个员工的详细信息列表,如名字、姓氏和地址。
如果你以这种方式声明数据,你可以将它们传递给zip()
并将所有内容包装在dict()
调用中。这将分配适当的键值对。代码如下:
employee_emails = ["bdoe@email.com", "mmarkson@email.com"]
employee_details = [
{
"first_name": "Bob",
"last_name": "Doe",
"address": {
"city": "New York",
"street": "1st Street",
"house_number": 1
}
},
{
"first_name": "Mark",
"last_name": "Markson",
"address": {
"city": "San Diego",
"street": "2nd Street",
"house_number": 2
}
}
]
employees = dict(zip(employee_emails, employee_details))
pp.pprint(employees)
结果数据看起来与之前的一样:
图像 2 — 员工的嵌套字典(2)(作者提供的图像)
实际上,没有理由使用这种方法来声明嵌套字典。这很混乱,编写起来也更费时间。只需坚持使用第一个方法,你就可以了。
接下来,让我们看看如何在 Python 中访问嵌套字典的元素。
如何访问嵌套字典的元素
你可以像访问普通字典一样访问嵌套字典的元素,唯一的例外是你现在需要添加额外的一组括号。
下面是你可以做的一些示例:
-
访问单个元素
-
访问一个也作为字典的单个元素
-
连接多个嵌套字典值
或者在代码中:
# Access element that contains a string
print(employees["bdoe@email.com"]["first_name"])
# Access element that contains a string
print(employees["bdoe@email.com"]["last_name"])
# Access element that contains a dictionary
print(employees["bdoe@email.com"]["address"])
# Combine multiple elements
print(f"{employees['bdoe@email.com']['first_name']} {employees['bdoe@email.com']['last_name']} from {employees['bdoe@email.com']['address']['city']}")
这是你应该看到的输出:
图像 3 — 访问嵌套 Python 字典的元素(作者提供的图像)
总的来说,如果你想到达嵌套字典的最底层,你需要与嵌套字典的层级数相同的括号。例如,要获取bdoe@email.com
的城市,你需要写employees["bdoe@email.com"]["address"]["email"]
。很简单!
如何更改嵌套字典中的值
你现在知道如何访问嵌套字典中的元素,但如何更改这些值呢?这非常简单,你可以逐个更改值,也可以一次处理多个值。
更改嵌套字典中的单个值
你可以通过访问嵌套字典并分配一个新值来更改单个值。
以下示例展示了如何一次更改一个员工的完整address
:
# Change values one by one
employees["bdoe@email.com"]["address"]["city"] = "San Francisco"
employees["bdoe@email.com"]["address"]["street"] = "5th Street"
employees["bdoe@email.com"]["address"]["house_number"] = 5
pp.pprint(employees["bdoe@email.com"])
更新后的员工数据现在如下所示:
图像 4 — 更改嵌套字典中的单个值(作者提供的图像)
很好,但你能在一行中更改 address
吗?当然可以,接下来我们将探讨如何实现。
在嵌套字典中更改多个值
address
属性本身就是一个字典,这意味着你可以在一行 Python 代码中完全更改它:
# Change multiple values at once
employees["mmarkson@email.com"]["address"] = {"city": "Los Angeles", "street": "15th Street", "house_number": 15}
pp.pprint(employees["mmarkson@email.com"])
更新后的员工记录如下:
图 5 — 更改嵌套字典中的多个值(图片作者提供)
你现在知道如何访问和更改嵌套字典元素了,接下来我们将讨论如何在 Python 中向嵌套字典中添加新元素。
如何向嵌套字典中添加元素
在 Python 中向嵌套字典中添加新元素就是赋值一个新的键值对。就是这么简单!
以下代码片段声明了两个变量来存储新的嵌套字典项的键和值,然后使用 dictionary[key] = value
赋值运算符添加这个键值对:
new_employee_email = "jswift@email.com"
new_employee_details = {
"first_name": "Jane",
"last_name": "Swift",
"address": {
"city": "Boston",
"street": "10th Street",
"house_number": 10
}
}
# dictionary[key] = value
employees[new_employee_email] = new_employee_details
pp.pprint(employees)
更新后的嵌套字典现在有 3 个记录:
图 6 — 在 Python 中向嵌套字典中添加元素(图片作者提供)
那是添加操作,所以接下来我们来讨论删除操作。
如何从嵌套字典中删除元素
你可以使用 Python 的 del
关键字,后跟字典的名称和你想删除的键。例如,运行 del d["name"]
来删除字典 d
中键为 name
的键值对。
在我们的示例中,让我们使用 del
删除刚刚添加的员工:
del employees["jswift@email.com"]
pp.pprint(employees)
现在我们只剩下两个记录了:
图 7 — 从嵌套字典中删除元素(图片作者提供)
接下来,让我们讨论如何合并两个或多个字典。
如何合并两个嵌套字典
字典合并就是将两个或多个字典合并成一个。字典的结构相同会有所帮助,但不是必须的,因为这是 Python。
为了演示,我们来声明一个新的嵌套字典的员工记录:
new_employees = {
"jswift@email.com": {
"first_name": "Jane",
"last_name": "Swift",
"address": {
"city": "Boston",
"street": "25th Street",
"house_number": 25
}
},
"pjohnson@email.com": {
"first_name": "Patrick",
"last_name": "Johnson",
"address": {
"city": "Miami",
"street": "50th Street",
"house_number": 50
}
}
}
pp.pprint(new_employees)
这就是它的样子:
图 8 — 两名新员工(图片作者提供)
这个想法是将这个字典添加到我们已有的字典中,有两种方法可以实现。
使用 update()
函数合并两个字典
update()
函数 更新 一个字典的内容到另一个字典。更新是就地进行的,这意味着你不需要重新赋值变量。
这是一个例子:
employees.update(new_employees)
pp.pprint(employees)
更新后的嵌套字典现在有四个记录:
图 9 — 合并嵌套字典(图片作者提供)
这个函数很容易使用,但缺点是 你一次只能添加一个字典。下一种方法则更灵活一些。
使用 kwargs 合并两个字典
**kwargs
方法对新手来说可能看起来很奇怪,但它本质上只是解包字典。通过这样做,你可以将尽可能多的字典解包到一个新的字典中。
这里是一个例子:
emps_merged = {**employees, **new_employees}
pp.pprint(emps_merged)
合并后的嵌套字典与我们之前的字典完全相同:
图 10 — 合并嵌套字典(2)(作者提供的图片)
最终,决定最佳合并方法还是取决于你。我们推荐使用**kwargs
,因为你可以在一行 Python 代码中合并数十个字典。
如何遍历嵌套字典
在 Python 中,处理嵌套字典时没有一种适用于所有情况的解决方案。字典项的结构会有所不同,这意味着你每次都需要定制代码。
为了演示,我们将介绍两个例子,一个较简单,另一个代码稍复杂一些。
第一个例子遍历所有字典项并打印键,然后也遍历相应的值并打印它。以下是代码:
# Keys and values
for emp_email, emp_info in employees.items():
print(f"EMAIL: {emp_email}")
# For each key that belongs to a dictionary at the given email
for key in emp_info:
# Print the corresponding key and value
print(f"{key} = {emp_info[key]}")
print()
你应该看到如下结果:
图 11 — 遍历嵌套字典(作者提供的图片)
如果你没有额外的嵌套层级,类似于我们所拥有的address
,这种方法可能有效。处理这个问题会更加具有挑战性,但接下来我们可以尝试一下。
更高级的迭代示例
现在的想法是进入address
字典,并打印它包含的所有元素。
为了让代码更健壮一点,我们将检查项目是否为字典,如果是,则遍历其项。如果项目不是字典,我们将简单地打印它:
# Keys and values
for emp_email, emp_info in employees.items():
print(f"EMAIL: {emp_email}")
# For every key in the inner dictionary
for key in emp_info:
# Check if a type is a dictionary
if type(emp_info[key]) is dict:
print(f"{key}:")
# Print nested items
for item in emp_info[key]:
print(f"\t{item} = {emp_info[key][item]}")
# Not a dictionary, print the value
else:
print(f"{key} = {emp_info[key]}")
print()
你应该在屏幕上看到如下内容:
图 12 — 遍历嵌套字典(2)(作者提供的图片)
总体而言,我们已经成功解析了我们的嵌套字典结构,但再次强调,这并不适用于所有嵌套字典。你必须定制代码片段以适应你的用例,这可能会很棘手且耗时。
如何扁平化嵌套字典
扁平化嵌套字典意味着你希望获得一个不包含任何字典或列表的字典。
这是在将嵌套 JSON 文档解析为 Pandas DataFrame 时非常常见的数据预处理技术。如果你正在处理这样的数据,你会知道输入数据的结构会有很大不同。
大多数情况下,你会有一个字典列表。我们将在下方声明这样一个列表,并在每个员工对象内部添加email
属性,而不是将其用作字典键:
employees = [
{
"first_name": "Bob",
"last_name": "Doe",
"email": "bdoe@email.com",
"address": {
"city": "New York",
"street": "1st Street",
"house_number": 1
}
},
{
"first_name": "Mark",
"last_name": "Markson",
"email": "mmarkson@email.com",
"address": {
"city": "San Diego",
"street": "2nd Street",
"house_number": 2
}
}
]
一旦你拥有这样的数据格式,就该进行扁平化处理了。以下递归函数将一个记录或列表中的一个字典元素进行扁平化。对于任何嵌套字典,它会将其扁平化,使键重命名为完整的路径。
flatten_dict()
函数必须应用于字典列表中的每一条记录,这意味着你可以使用 Python 循环或列表推导式。
这里有一个示例:
def flatten_dict(d: dict) -> dict:
out = {}
def flatten(x, name: str = ''):
if type(x) is dict:
for a in x:
flatten(x[a], name + a + '_')
elif type(x) is list:
i = 0
for a in x:
flatten(a, name + str(i) + '_')
i += 1
else:
out[name[:-1]] = x
flatten(d)
return out
# Apply the function to each row
employees_flat = [flatten_dict(emp) for emp in employees]
pp.pprint(employees_flat)
我们现在有了一个完全扁平的结构:
图 13 — 展开嵌套字典 (作者提供的图片)
请注意如何在其内部键值对之前添加了address
,这样我们仍然能够理解它最初所属的位置。
现在,当你有了一个扁平化的字典列表时,你可以将其转换为 Pandas DataFrame。
嵌套字典 Python 转 Pandas DataFrame
如果你想将嵌套字典转换为 Pandas DataFrame,你必须先将其扁平化。否则,你会得到奇怪的索引,并且可能会得到单个单元格的字典作为值。
让我们首先展示一个不良的做法,以便你能理解为什么要扁平化数据。下面是与我们在文章中使用的员工字典相同的字典。然后我们在调用pd.DataFrame()
时使用它:
import pandas as pd
employees = {
"bdoe@email.com": {
"first_name": "Bob",
"last_name": "Doe",
"address": {
"city": "New York",
"street": "1st Street",
"house_number": 1
}
},
"mmarkson@email.com": {
"first_name": "Mark",
"last_name": "Markson",
"address": {
"city": "San Diego",
"street": "2nd Street",
"house_number": 2
}
}
}
pd.DataFrame(employees)
这就是结果 DataFrame 的样子:
图 14 — 嵌套字典转 Pandas DataFrame (作者提供的图片)
糟糕且不可用。 你需要将一个扁平化的字典列表传递给 **pd.DataFrame()**
以将数据恢复为适当的格式。
你已经知道如何展开嵌套字典了,所以这应该感觉像是轻松的散步:
employees = [
{
"first_name": "Bob",
"last_name": "Doe",
"email": "bdoe@email.com",
"address": {
"city": "New York",
"street": "1st Street",
"house_number": 1
}
},
{
"first_name": "Mark",
"last_name": "Markson",
"email": "mmarkson@email.com",
"address": {
"city": "San Diego",
"street": "2nd Street",
"house_number": 2
}
}
]
# Flatten the records first
employees_flat = [flatten_dict(emp) for emp in employees]
pd.DataFrame(employees_flat)
现在,DataFrame 更容易理解和分析:
图 15 — 嵌套字典转 Pandas DataFrame (2) (作者提供的图片)
最后,让我们讲讲嵌套字典到 JSON 转换。
嵌套字典 Python 转 JSON 文件
JSON 和 Python 字典是相辅相成的,这意味着你可以轻松地在 JSON 文件和 Python 字典之间转换。
我们将展示如何将嵌套的 Python 字典转换为 JSON 文件。你需要导入json
模块,并将字典传递给json.dumps()
。可选的indent
参数控制字典内部嵌套结构的缩进。
下面是代码:
import json
employees = {
"bdoe@email.com": {
"first_name": "Bob",
"last_name": "Doe",
"address": {
"city": "New York",
"street": "1st Street",
"house_number": 1
}
},
"mmarkson@email.com": {
"first_name": "Mark",
"last_name": "Markson",
"address": {
"city": "San Diego",
"street": "2nd Street",
"house_number": 2
}
}
}
json_object = json.dumps(employees, indent=4)
print(json_object)
这就是你的 JSON 对象应该呈现的样子:
图 16 — 嵌套字典转 JSON 对象 (作者提供的图片)
你现在可以使用 Python 的上下文管理器语法将 JSON 对象写入文件。以下代码片段将其写入名为employees.json
的文件:
with open("employees.json", "w") as f:
json.dump(employees, f)
你可以在任何文本编辑器或 JupyterLab 中打开 JSON 文件。你会看到类似下面的内容:
图 17 — 嵌套字典转 JSON 文件 (作者提供的图片)
这就是你可以在 Python 中处理嵌套字典的方法。接下来,让我们简短回顾一下。
总结嵌套字典 Python
在 Python 中处理嵌套字典涉及很多内容。你可以访问单个值、修改它们、添加新行、删除旧行、合并多个字典、遍历它们,甚至将整个内容转换为 Pandas DataFrame 或 JSON 文件。
不幸的是,处理嵌套字典并没有一刀切的解决方案。每个项目的结构会有所不同,这意味着你需要定制代码以适应你的场景。特别是在遍历嵌套字典时,这一点尤其重要。
本文应该为你提供一个良好的起点,并涵盖 95%的场景,你可以随时自行深入探讨。
你对嵌套字典最喜欢的是什么?它们在日常数据科学任务中是否让你头疼? 请在评论区告诉我。
喜欢这篇文章?成为 Medium 会员 ,继续无限制地学习。如果你使用以下链接,我将获得你会员费用的一部分,而不会增加你的额外费用。
[## 使用我的推荐链接加入 Medium - Dario Radečić
阅读 Dario Radečić的每一个故事(以及 Medium 上成千上万其他作家的作品)。你的会员费用将直接支持…
medium.com](https://medium.com/@radecicdario/membership?source=post_page-----756a7822cb4f--------------------------------)
最初发表于 https://betterdatascience.com 于 2023 年 4 月 18 日。